@yawlabs/lemonsqueezy-mcp 0.13.1 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,539 +1,595 @@
1
- # Changelog
2
-
3
- ## [Unreleased]
4
-
5
- ### Changed
6
- - **The minimum oam version is now actually enforced.** `oamVersion()` and `atLeast()` were defined but never called, so `OAM_MIN` was dead code and any oam on the box was spawned regardless of version — including the pre-0.9.0 releases the floor exists to exclude, where `child_process.execFile` ran its arguments through a shell, `exec` accepted `timeout` and ignored it, and `stdio: 'inherit'` behaved as `'pipe'`. This launcher shells out on its main paths, so those were reachable bugs. An oam below the floor is now refused under `LEMONSQUEEZY_MCP_RUNTIME=oam` and bypassed for Node under `auto`, in both cases saying which version it found.
7
-
8
- ### Fixed
9
- - **The launcher no longer dies with a raw stack trace when `spawn` fails.** Node throws synchronously rather than emitting `error` for some unexecutable targets notably a `.cmd`/`.bat` on Windows and the `error` listener is registered *after* the `spawn` call, so it could never observe that throw. Both failure modes now route through one handler.
10
- - **Windows `PATH` discovery accepts `oam.exe` only**, instead of walking every `PATHEXT` entry and returning an `oam.cmd` Node cannot execute. A skipped shim is still **named** in the diagnostic, so an npm-style install no longer reports as "no oam binary was found".
11
- - **A failing in-process fallback no longer escapes as an unhandled rejection.** `void runInProcess()` discarded the promise, replacing the launcher's own diagnostic with a raw stack trace.
12
- - **Diagnostics that precede `process.exit` are written synchronously.** stderr is async for TTYs and pipes on Windows, so the exit could truncate them. They route through one helper that also handles short writes and macOS `EAGAIN` on a non-blocking piped stderr.
13
- - Removed a literal backspace byte (`U+0008`) from the runtime-discovery comment, which made git treat the file as binary so its diff could not be reviewed.
14
- - **Windows: the launcher no longer hard-kills the server on the first Ctrl-C.** There are no POSIX signals on Windows — `child.kill(sig)` ignores the name and calls `TerminateProcess`, an immediate hard kill (verified: a child with a `SIGTERM` handler never runs it and dies with `code=null`). The launcher forwarded anyway, on the stated assumption that this was a "no-op on Windows", so it aborted the graceful shutdown the console's own Ctrl-C had just started and skipped the server's `process.on("exit")` cleanup. The console already delivers the event to the whole process group, so on Windows the launcher now forwards nothing.
15
- - **A wedged server no longer leaves the launcher hanging.** Forwarding was gated on `child.killed`, which records only that `kill()` was *called* never that the child is gone so every signal after the first was swallowed and there was no escape hatch. Escalation is now armed by a timer on the first signal: one press is enough, and a child still alive after a 2s grace window is killed. Using a timer rather than counting signals also stops the ordinary supervisor sequence (`SIGINT` then `SIGTERM` milliseconds apart) from being misread as impatience.
16
-
17
- ## [0.12.0] — 2026-08-07
18
-
19
- ### Added
20
- - Runtime launcher at `bin/lemonsqueezy-mcp.mjs`: the published `lemonsqueezy-mcp` command now prefers the [oam](https://oamjs.org) runtime and falls back to Node. `LEMONSQUEEZY_MCP_RUNTIME` selects (`auto` / `oam` / `node`) and `OAM_BIN` overrides discovery. Both paths verified against the MCP surface — handshake plus all 64 tools — and behave identically. The fallback does **not** re-exec Node: npm has already started Node to run the launcher, so it is an in-process `import()` with no extra spawn.
21
-
22
- ### Changed
23
- - `.gitignore` excludes `bin/*` rather than `bin/`, so the launcher can be re-included with a negation. A negation cannot undo a directory-level exclusion that trap shipped a broken `bin` in postgres-mcp, where the launcher was untracked and absent from every fresh clone.
24
- - `scripts/build-binary.mjs` pins the CLI source entry instead of deriving it from `bin`'s value, which would have resolved to `bin/lemonsqueezy-mcp.ts` once `bin` moved to the launcher — the breakage postgres-mcp shipped in its 0.9.0.
25
-
26
- All notable changes to `@yawlabs/lemonsqueezy-mcp` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and versioning follows [SEMVER.md](./SEMVER.md).
27
-
28
- ## [0.11.1] -- 2026-08-07
29
-
30
- ### Fixed
31
-
32
- - **A 4xx whose error envelope carries no `detail` no longer dumps raw JSON at the caller.** LemonSqueezy's real 404 is `{"jsonapi":{"version":"1.0"},"errors":[{"status":"404","title":"Not Found"}]}` -- `title`, no `detail`. The message chain read `detail` then the License API's bare `error`, so this shape fell all the way through to the raw response body and an agent asking for a missing store got a JSON blob instead of a reason. `title` is now read between the two.
33
-
34
- Long-standing, not new in 0.11.0, and invisible to the unit suite because every fixture in it (and every one added during the 0.11.0 audit) used `detail`. Caught by a read-only call against the live API. Both shapes are now pinned, including the precedence when `detail` and `title` are both present.
35
-
36
- ## [0.11.0] -- 2026-08-07
37
-
38
- ### Security
39
-
40
- - **`redactSecrets` no longer degrades to exponential traversal on a shared-reference payload.** The cycle guard tracks the ancestor path (entries removed on the way back up) so a merely *shared* object is redacted normally instead of being falsely reported as `[CIRCULAR]` -- but an ancestor set alone is O(paths), not O(nodes). A "diamond chain" holding two references to the same child at every level has 2^depth paths over depth+1 objects: measured at 3.2 seconds for 23 objects, doubling per level, which at the 32-level cap would block the stdio server for the better part of an hour from inside the audit path. A memo of completed subtrees restores linear behaviour. Reuse is gated on the cached entry having been computed with at least as much depth budget as the current position, so a subtree truncated near the cap is never replayed into a shallower slot where real (possibly secret-bearing) input belongs.
41
-
42
- ### Added
43
-
44
- - **`validation_error` log/audit status.** Failure entries previously collapsed client mistakes into `exception`, the same bucket as upstream 5xx and genuine faults. The new `ToolInputError` is tagged `validation_error`, so a log scan at `LEMONSQUEEZY_LOG=error` can separate "an agent sent a malformed request" from "the server or upstream faulted". `guardrail_block` (operator policy refused it) is unchanged.
45
- - **`preflight` hook on the tool contract.** An input-dependent guardrail that runs ahead of the rate limiters. Both refund tools now declare the cap check there, so a rejected over-cap refund no longer consumes the caller's destructive and `money`-class budgets -- previously a client looping on an over-cap amount could exhaust a `money:2/h` allowance on calls that never left the process.
46
- - **Cross-store disclosure on `ls_list_stores`.** It has no `storeId` field and no parent ID to scope by, so `LEMONSQUEEZY_ALLOWED_STORE_IDS` does not gate it and it enumerates every store the API key can see. This was documented in the README but absent from the tool description an agent actually reads. `ls_list_affiliates` carried the note already.
47
-
48
- ### Fixed
49
-
50
- - **Empty-PATCH guards on the remaining three update tools.** `ls_update_customer`, `ls_update_subscription`, and `ls_update_license_key` sent `attributes: {}` upstream when called with only an ID; `ls_update_webhook` had rejected this locally since 0.10.10. All four now reject before the round-trip and throw `ToolInputError`.
51
- - **`ls_update_subscription` and `ls_update_customer` descriptions now disclose their conditional-destructive behaviour.** A tool whose destructiveness depends on the input carries `destructiveHint: false`, because the MCP annotation is static and the verdict is per-call -- so an MCP client will not prompt, and the description is the only signal left. `ls_update_subscription` said nothing about pausing or plan switches; `ls_update_customer` mentioned auditing only to point at `ls_archive_customer`, never saying that setting `status: 'archived'` *here* is itself rate-limited and audited. A new invariant in `tools.test.ts` fails the build if any predicate-carrying tool omits the warning.
52
- - **`Retry-After: " "` no longer means "retry immediately".** `Number("")` is `0`, not `NaN`, so a whitespace-only header fell through the numeric branch and produced a 0 ms wait instead of the 1 s default.
53
- - **`include: ""` no longer emits a bare `?include=`.** The include schemas set `.max()` but no `.min()`, so an empty string is valid input and split to `[""]`.
54
- - **Sink response size limit measures UTF-8 bytes, not UTF-16 code units.** `String.length` undercounts every multi-byte character, so a non-ASCII body could exceed the 10 MB limit while reporting a char count under it -- and the message still said "bytes".
55
- - **A bare `{"error": "..."}` envelope is now read on the management API**, not just the License API, via the shared error handler.
56
- - **A throwing `isDestructive` predicate can no longer escape as an unhandled rejection.** It is evaluated outside the main try block so it stays in scope for the audit path on the error branch; it now has its own guard that fails closed (treats the call as destructive) and logs the fault.
57
-
58
- ### Changed
59
-
60
- - **`apiRequest` and `licenseRequest` share one error path and one 2xx body reader.** Roughly 90 lines of near-identical parse/log/decorate logic collapsed to two helpers. `licenseRequest` still deliberately does not bust the API-key cache on 401, since it authenticates with the caller's license key rather than `LEMONSQUEEZY_API_KEY`.
61
- - **Both generate-invoice tools share one query builder** (`buildInvoiceQuery`) instead of duplicating an eight-field `URLSearchParams` block.
62
- - **Cross-store notes are generated from each module's `requiredFilters` array**, so the disclosure and the runtime gate cannot drift apart.
63
- - **`src/index.ts` is free of top-level await.** The version fallback used `await import("node:module")`, which the CJS single-binary build cannot emit; it survived only because both builds define `__VERSION__` and esbuild constant-folded the branch away. A static `createRequire` import removes the dependency on that folding -- verified by bundling to CJS with the define absent.
64
-
65
- ### Added
66
-
67
- - **Opt-in [oam.js](https://oamjs.org) build tooling.** `npm run check:oam` type-checks via `oam check` (tsgo, TypeScript 7 native) -- 2878ms against 4406ms for `tsc --noEmit`, same clean result. `npm run build:binary:oam` builds the standalone binary via `oam compile` instead of the Node SEA path, writing to the same `bin/<platform>-<arch>/` location so the release staging script consumes either unchanged. Neither touches the published npm package.
68
-
69
- **Node remains the default runtime.** oam runs the server unmodified (verified against 0.8.2: full handshake, all 64 tools, the audit-log resource, working `fetch`, guardrail errors identical to Node), but cold start measured 196ms for Node against 424ms for `oam run` -- and since an MCP client pays startup once per session, that is the cost that matters. oam is also not distributed on npm, so defaulting to it would break `npx` for anyone without it. `src/` stays runtime-agnostic (no `oam:` imports, tests on `node:test`) so the Node path remains a real fallback rather than a nominal one.
70
-
71
- - `oam/` added to `.gitignore` -- any `oam` invocation writes a bytecode cache there.
72
-
73
- ### Documentation
74
-
75
- - `0` documented as a valid value for the refund cap and both rate limits (blocks everything; unset/empty means no limit).
76
- - `LEMONSQUEEZY_LOG` row now lists what each failure `status` means.
77
- - README's allowlist row no longer describes `ls_list_stores` as routing "by its own resource ID"; the two genuinely ungated list tools are called out separately from the ID-targeted ones.
78
-
79
- ## [0.10.13] -- 2026-06-11
80
-
81
- ### Added
82
-
83
- - **Cross-platform single-binary pipeline** (Scoop + Homebrew) via `scripts/build-binary.mjs` and `scripts/stage-release-asset.mjs`.
84
-
85
- ### Fixed
86
-
87
- - **`npm test` runs every test file under POSIX sh**, not only nested ones -- an unquoted `**/*` glob skipped the top-level files.
88
-
89
- ### Changed
90
-
91
- - Node engine requirement raised to `>=22`; dev-dependency vulnerabilities cleared.
92
-
93
- ## [0.10.12] -- 2026-06-02
94
-
95
- ### Fixed
96
-
97
- - **Release drift guard compares tag-object SHAs**, so resuming a partial release no longer false-aborts.
98
- - **`SKIP_LINT=1` escape hatch** for environments where the lint runner itself is broken.
99
- - **`release.sh` refuses to push if origin's tag drifted from local.**
100
- - README "Add to Yaw MCP" badge points at the https forwarder.
101
-
102
- ### Added
103
-
104
- - End-to-end coverage for the refund-cap guard on both refund tools.
105
-
106
- ## [0.10.11] -- 2026-05-28
107
-
108
- ### Changed
109
-
110
- - **MCP Registry publish folded into `release.sh`**; `release.yml` and the remaining non-release workflows removed. `release.sh` is now the only release path.
111
-
112
- ### Fixed
113
-
114
- - Confirmation prompt is tty-gated, so non-interactive runs no longer block.
115
- - `server.json` syncs unconditionally rather than only inside the version-bump branch.
116
- - Falls back to the `gh` auth token for MCP Registry authentication.
117
-
118
- ## [0.10.10] -- 2026-05-22
119
-
120
- ### Fixed
121
-
122
- - **`release.sh` no longer demands a workstation npm session for CI-publish flows.** The pre-flight `npm whoami` gate was a vestige of the local-only-deploy era (added in `53de878`); when CI publishing was restored in `e3ef87a` the gate was kept but the publish itself moved to CI's org-level `NPM_TOKEN`, so the workstation never uses that credential. Blocked the v0.10.10 release attempt with "npm is not authenticated" despite the intended publish path not needing it. Mirrors `tailscale-mcp/release.sh`, which never had the gate.
123
-
124
- ### Security
125
-
126
- - **Webhook `url` restricted to http/https schemes.** `ls_create_webhook` and `ls_update_webhook` previously accepted any string up to 10k chars; `z.string().url()` alone would have additionally accepted every URL-parseable scheme (`mailto:`, `file:`, `javascript:`, `ftp:`, `chrome-extension:`, ...). Anything non-http(s) stored as a webhook target is unreachable in the best case and an injection sink in the worst. The new `httpsUrlSchema` chains `.url()` with a `.refine()` enforcing `^https?://`, applied on both tools.
127
- - **`ls_update_webhook` secret rotation gated as destructive.** Rotating the signing secret silently breaks signature verification on every receiver until they redeploy with the new value. The tool now declares an `isDestructive` predicate flagging any `secret` change, routing those calls through `checkDestructiveRateLimit` + audit logging via the wrapper's existing duck-typed integration. URL and events changes stay on the regular path.
128
-
129
- ### Changed
130
-
131
- - **`ls_update_webhook` rejects empty PATCH locally.** Calling with only `webhookId` and no fields to change previously sent an empty `attributes: {}` PATCH that the API would 422 with a less clear message. The handler now throws `"ls_update_webhook requires at least one of: url, events, secret"` before the round-trip.
132
- - **`ls_update_webhook` description warns about secret rotation.** The tool-level description now calls out that setting `secret` is destructive (rate-limited and audited) so LLM callers see the consequence before invoking.
133
-
134
- ### Tests
135
-
136
- - **Regression coverage for the `events.min(1)` fix** (`f7374bd`): schema-rejection tests on both `ls_create_webhook` and `ls_update_webhook` lock in the empty-array rejection.
137
- - **URL validation coverage**: non-URL string rejection on both tools, four non-http(s) schemes (`mailto:`, `file:`, `javascript:`, `ftp:`) rejected on both tools, http and https explicit acceptance.
138
- - **`ls_update_webhook` predicate tests** mirror the `ls_update_license_key` shape: secret-only as destructive, url-only and events-only as non-destructive, empty input as non-destructive.
139
- - **`ls_update_webhook` URL-encoding** test added in parallel to the existing `ls_delete_webhook` coverage.
140
- - **Handler-side empty-PATCH rejection** test asserts the new error message.
141
-
142
- ## [0.10.9] -- 2026-05-22
143
-
144
- ### Changed
145
-
146
- - **Webhook `events` array now requires at least one entry.** `ls_create_webhook` and `ls_update_webhook` previously let an empty array pass local Zod validation, so a no-op webhook configuration only failed at the LemonSqueezy API as a 422. The new `.min(1)` makes the rejection local with a clearer message; on update, the field stays optional but, when supplied, must be non-empty.
147
-
148
- ## [0.10.8] -- 2026-05-19
149
-
150
- ### Security
151
-
152
- - **Sink response body size guard extended to the error branch.** `sinkRequest` in `src/tools/sink.ts` previously only size-checked 2xx response bodies; a misbehaving sink returning a giant 4xx/5xx body could still buffer the whole thing into memory before any limit fired. A new `readBodyOrSizeError` helper pre-checks `Content-Length` against the 10 MB cap and is applied to both the error and success branches. The post-read length check on the 2xx path is retained as belt-and-braces against a lying `Content-Length`.
153
-
154
- ### Changed
155
-
156
- - **2xx body-read mid-stream failures now collapse to the uniform `{ ok: false, error }` shape.** Previously a socket reset partway through reading the body propagated as an exception out of `sinkRequest`, surfacing as a less-informative error via the wrapper's catch-all. The 2xx body read is now wrapped in a try/catch that returns `Sink response body read failed: <message>` -- consistent with every other failure mode in the function.
157
- - **`src/secret.ts` cache-hit branch tightened.** The test-mode cache-hit branch previously called `announceTestModeOnce()` redundantly (the flag is set on the first miss, so the cache-hit call was dead). Removed the call and the now-obsolete defensive comment.
158
-
159
- ### Tests
160
-
161
- - **Sink coverage expanded** to pin `authorityClass` per tool (`read` for `ls_sink_events_list` / `ls_sink_stats`, `mutate` for `ls_sink_event_mark_processed`), 4xx-oversized-Content-Length, 2xx-lying-Content-Length, and 2xx mid-stream body-read failures. `stubFetch` gained an optional `responseHeaders` field so error-branch tests no longer override `globalThis.fetch` inline.
162
- - **`parseCommand` contract pinned** in `src/secret.test.ts` for the four tokenizer edges that the rest of the suite only touched by accident: unterminated quote, all-quotes-collapse-to-empty, quoted-args-with-spaces (both quote styles), and quote-then-bare-word concatenation. Also added a 64 KB `maxBuffer` overflow test that exercises the `execFile` `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` rewrap path.
163
-
164
- ## [0.10.7] -- 2026-05-16
165
-
166
- ### Security
167
-
168
- - **Allowlist-bypass surface closed at the test layer.** `checkStoreScopedToolInput` in `src/wrapper.ts` enforces `LEMONSQUEEZY_ALLOWED_STORE_IDS` by reading the literal input field name `storeId` from the tool's Zod shape. Today every list tool that filters by store happens to use that name, but the convention wasn't asserted anywhere -- a future tool whose filterMap mapped a differently-named input (e.g. `store`) to `filter[store_id]` would silently bypass the allowlist. `src/api.ts` `listHandler` now exposes its `filterMap` on the returned handler, and `src/tools/tools.test.ts` adds an invariant that fails CI for any drift between the filter key (`store_id`) and the input field name (`storeId`).
169
-
170
- ### Changed
171
-
172
- - **`wrapper.ts` skips the success/error entry literal when no consumer wants it.** A non-destructive read at `LEMONSQUEEZY_LOG` unset (i.e. "off") previously built a full audit entry on every call, then `logEvent` discarded it inside its level check. Now the wrapper consults `wouldLogToolCall({ isDestructive, isError })` -- a new export from `src/logger.ts` -- before allocating. Destructive calls always build the entry because the audit-buffer push is independent of the log level. No observable behavior change; saves a small object literal per non-destructive read on the most common configuration.
173
-
174
- ## [0.10.6] -- 2026-05-16
175
-
176
- ### Security
177
-
178
- - **Sink response body size cap.** `sinkRequest` in `src/tools/sink.ts` now rejects responses larger than 10 MB before calling `JSON.parse`, defending the in-process MCP server against CPU exhaustion from a malicious or misconfigured sink (`Sink response body too large: X bytes exceeds Y byte limit`). The 10 s `AbortSignal` timeout already bounded wall-clock; the size cap bounds the parse cost as well.
179
-
180
- ### Changed
181
-
182
- - **`buildQuery` page-param values are URL-encoded.** Brought `page[number]` / `page[size]` in line with the `include` / `filter` branches (which already wrapped values in `encodeURIComponent`). Today the Zod schemas constrain both to integers so the encoding is a no-op, but if pagination ever widens to accept a string cursor the values are already safely escaped.
183
- - **`smithery.yaml` numeric fields are typed `integer`.** `lemonsqueezyMaxRefundAmountCents` and `lemonsqueezyDestructiveRateLimit` now declare `type: integer, minimum: 1` (were `type: string`). Smithery's config UI now renders a number input with client-side validation. The `commandFunction` `String(...)` coerces back to env-var form, and the truthy check switched to `!= null` so the (admittedly nonsensical) value `0` would not be silently dropped.
184
-
185
- ### Docs
186
-
187
- - **`CHANGELOG.md` em-dashes normalized to ASCII `--`** throughout v0.9.3 and earlier sections. Brings older entries in line with the v0.10.x style and avoids the Windows-terminal mojibake risk called out in the Yaw Mode discipline.
188
-
189
- ## [0.10.5] -- 2026-05-16
190
-
191
- ### Changed
192
-
193
- - **`release.sh` step 3 now bumps `server.json` alongside `package.json` and stages it in the release commit.** Previously `server.json` was treated as a derived artifact only -- `release.yml` ran a `jq` step at publish time to sync `version` and `packages[0].version` from `$GITHUB_REF_NAME`, but the committed file was never updated, so a manual `mcp-publisher publish` outside CI would push a stale registry version. The new `node -e` step mirrors the CI `jq` so the committed source matches the latest published version on every release. No `jq` dependency added (`node` was already a release-flow prereq).
194
- - **`server.json` resynced to 0.10.4** to clear the existing drift (file was last touched at 0.10.0; releases 0.10.1 through 0.10.4 all relied on CI's publish-time `jq` rewrite). 0.10.5 going forward, the workstation release path keeps it current automatically.
195
-
196
- ### Docs
197
-
198
- - **CHANGELOG footers backfilled** for v0.10.0 through v0.10.4. Adds the missing `[X.Y.Z]: .../compare/...` lines per Keep a Changelog and points `[Unreleased]` at `v0.10.4...HEAD` (previously stuck at `v0.9.3...HEAD`).
199
-
200
- ## [0.10.4] -- 2026-05-16
201
-
202
- ### Changed
203
-
204
- - **GitHub release notes now come from `CHANGELOG.md`, not just commit subjects.** `release.sh` step 6 extracts the section between `## [X.Y.Z] -- ...` and the next `## [` and passes that body to `gh release create --notes`. Falls back to the previous `git log --oneline` behavior if no matching section is present (or the section is whitespace-only), then to "Initial release". Removes the long-standing pattern of single-line "- vX.Y.Z" release bodies on the GitHub releases page (every 0.10.x release until this one).
205
-
206
- ## [0.10.3] -- 2026-05-16
207
-
208
- ### Fixed
209
-
210
- - **Startup env-var parse errors now print a single line, not a stack trace.** The 0.10.1 eager-parse of `LEMONSQUEEZY_DISABLE_CLASSES` / `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS` / `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` threw an uncaught `Error`, so operators saw Node's stack dump before the helpful message. `src/index.ts` now wraps the call in try/catch and exits 1 with just the error message -- e.g. `LEMONSQUEEZY_DISABLE_CLASSES contains unknown class "mony" (expected one of: read, pii, mutate, money, recurring, key, webhook)`.
211
-
212
- ## [0.10.2] -- 2026-05-16
213
-
214
- ### Changed
215
-
216
- - **`CHANGELOG.md` is now included in the published npm tarball.** Added it to `package.json`'s `files` array. npm auto-includes `README` and `LICENSE` regardless of the allowlist, but `CHANGELOG.md` is not in that auto-include set -- so prior versions shipped without it, and an operator inspecting `node_modules/@yawlabs/lemonsqueezy-mcp` had no in-package changelog. The README and bundle are unchanged; tarball size grows by ~29 kB.
217
-
218
- ## [0.10.1] -- 2026-05-16
219
-
220
- ### Fixed
221
-
222
- - **`smithery.yaml` now exposes the sink-bridge env vars.** Added `lemonsqueezySinkUrl` and `lemonsqueezySinkAdminToken` to `configSchema.properties` and forwarded them in `commandFunction`. Without this, Smithery-installed instances could not configure the 0.10.0 `ls_sink_*` tools and they always returned "not configured".
223
- - **Guardrail env vars are now parsed at server boot, not first call.** A new `loadGuardrailOptions()` export is invoked from `src/index.ts` before stdio connect, so a typo'd `LEMONSQUEEZY_DISABLE_CLASSES`, malformed `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS`, or non-numeric `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` crashes the process at startup. Previously the parse was lazy and a misconfigured deployment booted cleanly, passing liveness probes, and only surfaced the error on the first tool invocation.
224
-
225
- ## [0.10.0] -- 2026-05-15
226
-
227
- ### Added
228
-
229
- - **Webhook-sink bridge tools** for the optional [@yawlabs/lemonsqueezy-webhook-sink](https://github.com/YawLabs/lemonsqueezy-webhook-sink) process. Three new tools give an agent a unified read surface for "what webhooks have actually fired" alongside the existing management-API reads:
230
- - `ls_sink_events_list` -- list received webhook events with optional `since` / `type` / `limit` filters
231
- - `ls_sink_event_mark_processed` -- idempotent ack from your consumer
232
- - `ls_sink_stats` -- total, unprocessed, last-received timestamp
233
- - `LEMONSQUEEZY_SINK_URL` and `LEMONSQUEEZY_SINK_ADMIN_TOKEN` env vars to configure the bridge. Tools are always registered for `tools/list` discovery; missing env vars surface at call time as a structured "not configured" error (with a pointer to the sink repo), not a registration-time failure. 10s fetch timeout via `AbortSignal.timeout()`; auth failures (401), admin-disabled (404), and transport errors each get a tailored diagnostic.
234
-
235
- ## [0.9.3] -- 2026-05-15
236
-
237
- ### Fixed
238
-
239
- - `server.json` rewritten against the current registry schema (`2025-12-11`). The 0.9.2 release's MCP-Registry publish step failed validation against the live schema: `description` exceeded the 100-char cap, the package field was `registry_type` (snake_case) where the schema expects `registryType` (camelCase), and the required `packages[].transport` field was missing. 0.9.2 is live on npm and GitHub but did not reach the Official MCP Registry. 0.9.3 is the first release that should land on `registry.modelcontextprotocol.io`.
240
-
241
- ## [0.9.2] -- 2026-05-15
242
-
243
- ### Added
244
-
245
- - `.github/workflows/release.yml` now publishes to the [Official MCP Registry](https://registry.modelcontextprotocol.io) after the post-publish smoke test passes. Authentication is via GitHub OIDC -- the `id-token: write` permission already enabled for npm provenance also satisfies the registry's auth; no `MCP_*` secret is required. The namespace `io.github.YawLabs/*` is authorized purely from the OIDC `repository_owner` claim, so anyone outside the YawLabs GitHub org cannot publish under it. A `jq` step overwrites `server.json`'s `version` (and `packages[0].version`) from the pushed tag, so a forgotten manual bump on `server.json` no longer publishes a stale version to the registry. Downstream registries (Glama, PulseMCP, mcpservers.org) that auto-source from the official registry now pick up each `@yawlabs/lemonsqueezy-mcp` release without a manual mcp-publisher run.
246
-
247
- ### Docs
248
-
249
- - README Resources section now spells out the full redaction policy (every secret-named key the regex matches, plus JWT-shaped string values), so an operator who sees a `[REDACTED]` under an innocuous key knows where it came from.
250
- - README Development section documents `npm run gen:containerfile` / `npm run check:containerfile` so a contributor editing `Dockerfile` knows to regenerate.
251
- - README Releasing section notes that the local `./release.sh` path does not publish to the Official MCP Registry (CI-only) and gives the manual `mcp-publisher` commands as the fallback when a release is cut without CI.
252
-
253
- ## [0.9.1] -- 2026-05-15
254
-
255
- ### Docs
256
-
257
- - README "Features" list now mentions authority-class disable, per-class rate limits, and the audit-log MCP Resource -- previously omitted under the umbrella "Guardrails" bullet despite being documented in detail later in the file.
258
- - README description of `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` now lists every destructive trigger the runtime classifier counts: `ls_update_license_key` calls that change `activationLimit` (added in 0.7.1), `ls_update_customer` calls with `status: "archived"`, plus the previously-documented pause/plan-switch and `disabled: true` paths.
259
- - README qualifies the "issue a scoped LemonSqueezy API key" recommendation. LS API keys are account-wide; the qualifier directs readers to LS team-membership scoping where it exists and treats the env-var guardrails as the primary control surface for what LS itself can't express (per-class rate ceilings).
260
-
261
- ### Changed
262
-
263
- - `audit-buffer.ts` overflow path is now O(1) (circular index) instead of O(N) (`Array.splice(0, …)`). No observable behavior change; the buffer caps at 1000 entries and presents the same most-recent-first read order.
264
- - `src/index.ts` split -- the registered MCP tool wrapper and the audit-log Resource read-callback now live in `src/wrapper.ts` so unit tests can exercise them end-to-end without starting the stdio MCP server. `src/index.ts` becomes a thin entry point that imports `createToolHandler` and `readAuditLogResource` from the new module. No observable behavior change in the published binary.
265
- - `secret.ts` cache fingerprint no longer embeds the raw API key. The fingerprint is now a SHA-256 digest of `mode:value`, preserving the mid-process change-detection semantics while removing the only in-memory copy of the key outside the `cached.key` field itself.
266
- - `redact.ts` now also masks any string value matching the LemonSqueezy bearer-token shape (`eyJ…`-style JWT prefix), even when the key name is innocuous. Closes the future-feature gap where a destructive tool accepts a `customData`/`metadata` object whose values happen to contain a key. Also covers additional credential / PII key names (`private_key`, `pin`, `ssn`, `social_security_number`, `credit_card`, `card_number`, `cvv`, `cvc`).
267
-
268
- ### Fixed
269
-
270
- - Smithery one-click install now exposes `LEMONSQUEEZY_DISABLE_CLASSES` and `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS` -- the two env vars added in 0.9.0. Previously a Smithery user had to hand-edit the spawned process env to reach the authority-class layer.
271
- - Smithery description for `lemonsqueezyDestructiveRateLimit` now states the actual format (a positive integer interpreted as calls per 60-second rolling window) instead of "Format depends on server defaults."
272
- - `logger.ts` JSON-stringify fallback now coerces `entry.error` to a plain string before the second serialization pass, so a non-string error field (an `Error` with a circular `cause` chain, or an object with a throwing `toJSON`) no longer skips both write attempts and degrades to silence. Tests cover the case.
273
- - `integration.test.ts` `before()` hook now clears `LEMONSQUEEZY_API_KEY_COMMAND` and `LEMONSQUEEZY_TEST_API_KEY` before setting `LEMONSQUEEZY_API_KEY` from the test key, and resets the secret cache. Previously a developer with a vault-backed `LEMONSQUEEZY_API_KEY_COMMAND` would silently run the integration suite against their dev key, because the command takes precedence over the bare env var in `secret.ts`.
274
- - `Containerfile` is now generated from `Dockerfile` via `scripts/sync-containerfile.mjs`. `npm run check:containerfile` (called by `release.sh` and CI) verifies the two stay in sync; `npm run gen:containerfile` regenerates. Removes the byte-identical-but-not-actually drift surface where editing one file silently left the other behind.
275
-
276
- ### Added
277
-
278
- - `src/wrapper.ts` -- extracted production module containing `createToolHandler` (the MCP tool registration wrapper) and `readAuditLogResource` (the audit-log Resource read-callback). Same logic that previously lived inline in `src/index.ts`; now reusable and testable.
279
- - `src/wrapper.test.ts` -- end-to-end test that exercises the registered MCP tool wrapper from `src/wrapper.ts`. Covers the order in which `checkClassAllowed` / `checkClassRateLimit` / `checkDestructiveRateLimit` / `checkStoreScopedToolInput` fire, the destructive-call audit pipeline (logger + ring buffer), the audit-log MCP Resource (`lemonsqueezy://audit-log`), and the predicate-destructive routing (e.g. `ls_update_license_key` with `activationLimit`). Previously every handler test invoked `tool.handler(input)` directly and the wrapper was untested.
280
-
281
- ## [0.9.0] -- 2026-05-14
282
-
283
- ### Added
284
-
285
- - **Authority classes.** Every tool now declares an `authorityClass` describing the kind of business authority a caller needs to invoke it (`read`, `pii`, `mutate`, `money`, `recurring`, `key`, `webhook`). The class is separate from the existing destructive/read-only annotation.
286
- - `LEMONSQUEEZY_DISABLE_CLASSES` -- comma-separated list of classes to refuse outright. Example: `money,recurring,pii` lets an agent run reads but blocks refunds, subscription changes, and customer-record access. Unknown class names throw at server startup.
287
- - `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS` -- per-class rolling rate limits with a DSL. Each entry is `class:N`, `class:N/m`, or `class:N/h`. Example: `money:2/h,recurring:5/h,key:10/m`. Composes with `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` -- both must pass. No LemonSqueezy permission can express "max 2 refunds per hour"; this is the only place that policy can live.
288
- - `deprecate.yml` workflow -- CI-driven `npm deprecate` via `workflow_dispatch`, using the org-level `NPM_TOKEN` and the same `release-npm` concurrency group as `release.yml`. No local WebAuthn session needed for a deprecate run.
289
- - `server.json` + `mcpName` field in `package.json` -- prepares the repo for the Official MCP Registry. Downstream registries (Glama, PulseMCP, mcpservers.org) auto-source from the official registry, so a single publish reaches the ecosystem.
290
- - README "Add to mcp.hosting" install button -- one-click sync of the server into [mcp.hosting](https://mcp.hosting) so it propagates to every MCP client a user has configured.
291
-
292
- ### Changed
293
-
294
- - README "Authority classes" section reframed to lead with the authoritative control (a scoped LemonSqueezy API key) and present the env vars as defense in depth. `RATE_LIMIT_PER_CLASS` is called out as load-bearing (no LS access-control equivalent); `DISABLE_CLASSES` is positioned as a fast deploy-time alternative when the cost of a key rotation outweighs the strength gained.
295
- - `release.yml` smoke test now retries the actual `npx --version` call (30 × 10s) instead of gating on `npm view`. The two paths hit different CDN caches; `npm view` can clear while `npx` still ETARGETs on a stale mirror. Mirrors the pattern in `aws-mcp` / `tailscale-mcp`.
296
-
297
- ## [0.8.1] -- 2026-05-13
298
-
299
- ### Fixed
300
-
301
- - `handlers.test.ts` isolation: CI's integration job sets `LEMONSQUEEZY_TEST_API_KEY` from a repo secret. With the priority chain introduced in 0.8.0 (`COMMAND` > `TEST_API_KEY` > `API_KEY`), that injected value took precedence over the in-test stub and broke three handler tests (`ls_get_user` bearer-token assertion plus the missing/empty-key cases). The suite-level `before()` now saves and clears all three source env vars, restores them in `after()`, and resets the secret cache. Reproducible locally by running with `LEMONSQUEEZY_TEST_API_KEY=ci-stub-key LEMONSQUEEZY_API_KEY="" npm test`.
302
-
303
- ## [0.8.0] -- 2026-05-13
304
-
305
- Distribution-readiness pass -- closes feature gaps against other OSS LemonSqueezy MCP servers ahead of broader distribution.
306
-
307
- ### Added
308
-
309
- - **Smithery one-click install.** `smithery.yaml` declares the stdio start command + JSON Schema for env vars; Smithery generates the config UX. Install via `npx -y @smithery/cli install @yawlabs/lemonsqueezy-mcp --client claude`.
310
- - **Container images.** `Dockerfile` + `Containerfile` + `.dockerignore`. Multi-stage build on `node:20-alpine`, runs as the non-root `node` user, copies only `dist/index.js` into the runtime image. Stdio transport; no port exposed. Pass env via `-e LEMONSQUEEZY_*`.
311
- - `LEMONSQUEEZY_TEST_API_KEY` -- separate test-mode key. Priority sits between `LEMONSQUEEZY_API_KEY_COMMAND` (highest) and `LEMONSQUEEZY_API_KEY` (lowest), so a developer can point the server at a sandbox store without unsetting their production key. A one-shot stderr `test_mode` notice fires on first activation per process.
312
- - **MCP Resource `lemonsqueezy://audit-log`** -- bounded ring buffer (last 1000 entries, most-recent-first) of destructive-call audit entries, exposed as `application/x-ndjson`. Lets clients without stderr access retrieve the audit trail structurally. Secret-shaped input fields are already redacted before they reach the buffer.
313
-
314
- ### Changed
315
-
316
- - Dev-dependency audit warnings cleared via `npm audit fix`. Runtime bundle is unaffected (zero runtime deps).
317
-
318
- ## [0.7.1] -- 2026-05-13
319
-
320
- Hardening pass on the v0.4.0 guardrails after observing realistic agent failure modes.
321
-
322
- ### Added
323
-
324
- - **Retry deadline.** `OVERALL_DEADLINE_MS=90s` ceiling on total wall clock across retries. The timeout error reports actual elapsed and attempt count (`Request timed out after Xs (N attempts)`) instead of the per-attempt budget. The loop stops on 5xx/429/timeout once the deadline is reached rather than starting a new attempt or sleeping past it.
325
- - **Audit redaction (`src/redact.ts`).** Masks values of secret-shaped keys (`secret`, `password`, `token`, `api[_-]?key`, `bearer`, `authorization`, `signing[_-]?secret` -- case-insensitive, whole-word) before the entry reaches the audit log. Anchored regex so business identifiers (`licenseKey`, `instanceId`, `storeId`) are preserved. Cycle-safe via `WeakSet` with a 32-depth cap. Defense in depth -- no destructive tool today carries a secret-typed input, but flipping any webhook tool to destructive would otherwise leak its signing secret.
326
- - **`requiredFilters` scoping.** When `LEMONSQUEEZY_ALLOWED_STORE_IDS` is active, list tools that lack a `storeId` field must specify at least one parent filter (orderId, productId, variantId, etc.). Closes the silent-no-op gap on `ls_list_files` / `ls_list_prices` / `ls_list_variants` / `ls_list_order_items` / `ls_list_subscription_items` / `ls_list_usage_records` / `ls_list_discount_redemptions` / `ls_list_license_key_instances`. Each affected tool description flags the cross-store consideration. `ls_list_affiliates` has no meaningful parent filter and is description-only.
327
-
328
- ### Changed
329
-
330
- - `ls_update_license_key` is now classified as destructive whenever `activationLimit` changes, not just when `disabled: true`. Shrinking the limit can revoke access and the input alone doesn't reveal shrink-vs-grow direction.
331
- - `ls_create_checkout` email validation tightened to `.email().max(320)` to match `ls_create_customer` (was `max(10000)` with no shape check).
332
-
333
- ## [0.7.0] -- 2026-05-06
334
-
335
- ### Added
336
-
337
- - `LEMONSQUEEZY_LOG=audit|error|all` -- fine-grained log levels for long-running deployments. `audit` keeps destructive-call entries plus errors and drops successful reads (recommended for production where log volume matters over weeks). `error` keeps only failures. `all` is the verbose default. `json` is retained as a backwards-compat alias for `all`.
338
-
339
- ### Changed
340
-
341
- - Tool ID validation now uses a shared `lsIdSchema` (numeric string regex) so a typo fails at the schema layer with a clear message instead of as an opaque 422 from the upstream API.
342
- - `ls_update_customer.status` tightened to `z.literal("archived")` so the destructive predicate cannot be side-channeled by an unrecognized status string.
343
- - Secret cache refactored to a fingerprint-keyed entry. `invalidateApiKeyCache()` (called from `api.ts` on 401/403 responses) now behaves uniformly across the env, test, and command source modes. A rotated upstream key picks up on the next request without waiting on the 1h TTL.
344
- - Integration-test unique suffix switched from a timestamp slice to `crypto.randomUUID().slice(0, 8)`. Two overlapping CI runs (nightly + `workflow_dispatch`) that started in the same millisecond no longer collide on resource naming.
345
-
346
- ### Fixed
347
-
348
- - `release.sh` annotated tags (`git tag -a`) -- `git push --follow-tags` silently skips lightweight tags. Pre-0.7.0, the release commit could push without the tag, leaving CI release unfired. Caught by review before any failed run.
349
- - `release.sh` idempotency check now queries `@yawlabs/<pkg>@${VERSION}` specifically rather than the package's `latest` dist-tag. The bare query returned whichever version is latest on the registry, so an out-of-band higher version made the script try to re-publish the current one and fail with "cannot publish over previously published version." The versioned form returns the version when it exists and empty otherwise -- correct idempotency semantics.
350
- - `release.sh` push now uses `--follow-tags` instead of `--tags` so stale local tags don't ride along (`--tags` pushes every local tag).
351
- - `release.yml` concurrency group is now a literal `release-npm` (workflow-scoped); previously `release-${{ github.ref }}` evaluated to per-tag groups and back-to-back tag pushes would race on `npm publish`.
352
- - `ci.yml` no longer runs a redundant explicit `npm run build` step -- `npm test` is `npm run build && node --test dist/...` for this dist-based repo, so the explicit step was building twice per matrix cell for zero signal.
353
-
354
- ### Restored
355
-
356
- - `CODEOWNERS` and `dependabot.yml` (dropped along with the workflows in 0.6.0; restored when the workflows came back in 0.6.1). Routes review requests to `@jeffyaw` and bumps npm deps weekly / github-actions deps monthly.
357
-
358
- ## [0.6.2] -- 2026-05-04
359
-
360
- ### Changed
361
-
362
- - `release.yml` now runs a post-publish smoke test that fetches the just-published tarball via `npx -y @yawlabs/lemonsqueezy-mcp@<version> --version` and asserts the binary executes and prints the expected version. Catches packaging regressions (missing bin shebang, broken `files` entry, bad esbuild output) before they reach real users.
363
- - `release.sh` step 7 now verifies that CI publishes carry a sigstore provenance attestation. A missing attestation in CI mode is a soft warning; local publishes legitimately skip provenance.
364
-
365
- ## [0.6.1] -- 2026-05-04
366
-
367
- ### Changed
368
-
369
- - Restored CI release plumbing (`ci.yml`, `integration.yml`, `release.yml`) ported from `tailscale-mcp`. Tag-and-let-CI is the preferred release path; local `release.sh <version>` still works for end-to-end runs from the workstation.
370
- - `release.sh` gained a CI mode that derives version from `$GITHUB_REF_NAME`, skips local-only gates, and publishes with `--provenance`.
371
-
372
- ### Fixed
373
-
374
- - `integration.yml` now requires both `LEMONSQUEEZY_TEST_API_KEY` and `LEMONSQUEEZY_TEST_STORE_ID` together. A half-configured repo previously ran the workflow successfully with every integration suite silently skipped, masking zero coverage as green CI.
375
- - `release.sh` CI mode now hard-fails when `package.json` disagrees with the pushed tag instead of bumping inside the ephemeral checkout, which would have published the right version while leaving `main` pointing at the old one.
376
- - `release.sh` `npm publish` retry only fires on EOTP/EAUTH/OTP messages. Other failures (duplicate-version E403, packaging errors) bail immediately instead of wasting 60s in the retry loop.
377
- - `package.json` `prepublishOnly` trimmed to `npm run build`. `release.sh` already runs lint + tests before publishing in both modes, so the embedded test run was doubling the CI work per release.
378
-
379
- ## [0.6.0] -- 2026-04-24
380
-
381
- ### Changed
382
-
383
- - **Breaking.** `LEMONSQUEEZY_ALLOWED_STORE_IDS` now requires `storeId` on list tools that accept it as a filter (`ls_list_orders`, `ls_list_subscriptions`, etc.). Previously the allowlist was silently bypassed when callers omitted the optional filter, returning data from every store the API key could see. Callers that relied on the unfiltered behavior must now pass an allowed `storeId` explicitly.
384
- - `ls_update_license_key` calls that set `disabled: true` are now classified as destructive at runtime, so revocations engage `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` and the audit log. Benign edits (expiry, activation limit) stay on the regular path.
385
- - `ls_update_subscription` rejects non-numeric `variantId` at the schema level instead of producing `NaN` upstream.
386
- - `release.sh` rewritten as a local-only, idempotent deploy with branch and auth pre-flight, EOTP retry on `npm publish`, and clearer failure messages. Re-running with the same version after a partial failure resumes from where it stopped.
387
-
388
- ### Fixed
389
-
390
- - Path segments are URL-encoded across `getHandler` and every inline tool handler via a new `encodePath()` helper, closing a path-injection surface where IDs containing `/` could target adjacent endpoints.
391
- - Logger emits a degraded fallback entry on `JSON.stringify` failure so destructive-call audit trails survive circular inputs.
392
- - Checkout `billing_address` composition is now order-independent.
393
- - Integration test env mutation moved from module-eval to a `before()` hook.
394
-
395
- ### Removed
396
-
397
- - All of `.github/` (workflows, dependabot, CODEOWNERS). There is no CI -- `release.sh` is the only supported release path.
398
- - `test:ci` npm script.
399
-
400
- ### Docs
401
-
402
- - README documents the local release flow and the one-time `npm login` / `gh auth login` setup.
403
- - README clarifies `LEMONSQUEEZY_ALLOWED_STORE_IDS` semantics -- list filters now required when the allowlist is set; tools with no `storeId` field at all remain ungated, so pair with the refund cap and rate limit.
404
- - README notes that `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` counts include `ls_update_license_key` with `disabled: true`.
405
- - `SEMVER.md` points at `npm run test:integration` for upstream-drift detection instead of the removed nightly workflow.
406
- - `CLAUDE.md` and `CONTRIBUTING.md` updated to reference the local script and Biome-on-review instead of CI checks.
407
-
408
- ## [0.5.0] -- 2026-04-23
409
-
410
- ### Changed
411
-
412
- - **Breaking.** `ls_update_webhook` now validates `secret` as `min(6).max(40)`, matching `ls_create_webhook`. Previously accepted any string up to 10,000 chars. Callers passing secrets outside the 6-40 range will now be rejected at the MCP boundary.
413
- - **Breaking.** Email fields across `ls_list_customers`, `ls_create_customer`, `ls_update_customer`, `ls_list_orders`, and `ls_list_subscriptions` now validate as RFC email (`z.string().email().max(320)`), matching the existing `ls_list_affiliates` filter. Non-email inputs (partial matches, malformed addresses) will now be rejected at the MCP boundary.
414
- - `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` guardrail now also applies to `ls_refund_subscription_invoice`, closing a gap where the cap only gated `ls_refund_order`.
415
- - `ls_update_subscription` `trialEndsAt` is now `.nullable()` at the schema level. The description always promised `null` ends the trial immediately; the type now supports it.
416
-
417
- ### Fixed
418
-
419
- - Integration test env-var restoration now runs in a top-level `after()` hook instead of at module scope, so `LEMONSQUEEZY_API_KEY` stays set to the test key for the duration of the test run. Previously a developer with both `LEMONSQUEEZY_API_KEY` (prod) and `LEMONSQUEEZY_TEST_API_KEY` set could have their prod key used against the test store.
420
-
421
- ### Docs
422
-
423
- - README tool count corrected from 59 to 61 (affiliate tools from 0.3.0 plus `ls_refund_subscription_invoice` weren't reflected in the total).
424
-
425
- ## [0.4.1] -- 2026-04-20
426
-
427
- ### Security
428
-
429
- - Override transitive `hono` to `^4.12.14` to clear Dependabot advisory on `hono/jsx` SSR. Not exploitable in this package (MCP does not use `hono/jsx`), but closes the supply-chain scan signal.
430
-
431
- ### Docs
432
-
433
- - README links to `@yawlabs/lemonsqueezy-webhook-sink` from the webhook-reconciliation callout.
434
-
435
- ## [0.4.0] -- 2026-04-20
436
-
437
- Hardening pass for unattended automation against live billing flows.
438
-
439
- ### Added
440
-
441
- - **Guardrails.** Opt-in controls evaluated in a single dispatcher pre-check:
442
- - `LEMONSQUEEZY_ALLOWED_STORE_IDS` -- allowlist enforced on every tool call that names a store.
443
- - `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` -- per-call cap on `ls_refund_order` to prevent runaway agents from issuing large refunds.
444
- - `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` -- rolling 60-second circuit breaker on destructive tool calls.
445
- All three default to unset → disabled, so existing integrations are unaffected until explicitly opted in.
446
- - **Retry layer** (`src/retry.ts`). Exponential backoff with jitter, capped at 4 attempts and 30s. Retries `429` always (honors `Retry-After`), `5xx` and transport errors only on idempotent methods (`GET`/`DELETE`). Non-idempotent writes fail fast.
447
- - **Secret loader** (`src/secret.ts`). `LEMONSQUEEZY_API_KEY_COMMAND` invokes an external command (vault CLI, 1Password, etc.) and caches the result for 1 hour. Lets credentials rotate without restarting the server.
448
- - **Structured logger** (`src/logger.ts`). Opt-in via `LEMONSQUEEZY_LOG=json`. Emits JSON lines to stderr (stdout stays reserved for MCP protocol). Destructive tool calls are tagged `audit: true` with redacted inputs.
449
- - **Request ID surfacing.** API error messages include upstream `X-Request-Id` when present, so support tickets can be traced.
450
- - **Read-only integration tests.** `npm run test:integration` hits a live LemonSqueezy store if `LEMONSQUEEZY_TEST_API_KEY` + `LEMONSQUEEZY_TEST_STORE_ID` are set; skips gracefully otherwise. Exercises `ls_get_user`, `ls_get_store`, `ls_list_products`, `ls_list_variants`, `ls_list_orders`, `ls_list_subscriptions`, and 404 error paths. Runs nightly via `.github/workflows/integration.yml`.
451
- - **`SEMVER.md`** -- documents what counts as a breaking change for this package (tool names, required inputs, return shapes), and what explicitly does not (upstream API drift, internal module names).
452
-
453
- ### Changed
454
-
455
- - Input validation on every tool: `z.string()` fields capped at 10,000 chars, `.email()` fields capped at 320 chars. Protects against accidental DoS via oversized payloads.
456
-
457
- ### Fixed
458
-
459
- - `parseRetryAfterMs` now correctly falls back to the default 1s when given a negative number like `"-3"` (previously `Date.parse("-3")` returned a finite value and produced a non-sensical delay).
460
- - Retry policy no longer retries `5xx` on `POST`/`PATCH`/`PUT` -- prevents duplicate writes if a timeout is actually a slow success.
461
-
462
- ## [0.3.0] -- 2026-04-18
463
-
464
- ### Added
465
-
466
- - Affiliate tools (`ls_list_affiliates`, `ls_get_affiliate`).
467
- - `429` retry with exponential backoff in the API client.
468
- - `SECURITY.md` -- vulnerability disclosure policy.
469
- - `CONTRIBUTING.md` -- contributor and AI-agent guidelines.
470
-
471
- ## [0.2.1] -- 2026-04-16
472
-
473
- ### Changed
474
-
475
- - Deduplicated tool handler boilerplate across tool files.
476
-
477
- ### Fixed
478
-
479
- - API error handling surfaces upstream error bodies correctly.
480
-
481
- ### Added
482
-
483
- - Error-path tests for every tool.
484
-
485
- ## [0.2.0] -- 2026-04-14
486
-
487
- ### Added
488
-
489
- - Input validation via Zod `.describe()` on every tool input field.
490
-
491
- ### Fixed
492
-
493
- - `ls_generate_order_invoice` and `ls_generate_subscription_invoice` now hit the correct endpoints and handle the async invoice-generation response shape.
494
-
495
- ## [0.1.1] -- 2026-04-12
496
-
497
- ### Added
498
-
499
- - Edge-case handler tests for fuller coverage across all 59 tools.
500
-
501
- ## [0.1.0] -- 2026-04-11
502
-
503
- Initial release. 59 tools covering all 17 LemonSqueezy API resources.
504
-
505
- [Unreleased]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.9...HEAD
506
- [0.11.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.11.0...v0.11.1
507
- [0.11.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.13...v0.11.0
508
- [0.10.13]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.12...v0.10.13
509
- [0.10.12]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.11...v0.10.12
510
- [0.10.11]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.10...v0.10.11
511
- [0.10.10]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.9...v0.10.10
512
- [0.10.9]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.8...v0.10.9
513
- [0.10.8]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.7...v0.10.8
514
- [0.10.7]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.6...v0.10.7
515
- [0.10.6]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.5...v0.10.6
516
- [0.10.5]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.4...v0.10.5
517
- [0.10.4]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.3...v0.10.4
518
- [0.10.3]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.2...v0.10.3
519
- [0.10.2]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.1...v0.10.2
520
- [0.10.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.0...v0.10.1
521
- [0.10.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.9.3...v0.10.0
522
- [0.9.3]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.9.2...v0.9.3
523
- [0.9.2]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.9.1...v0.9.2
524
- [0.9.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.9.0...v0.9.1
525
- [0.9.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.8.1...v0.9.0
526
- [0.8.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.7.1...v0.8.1
527
- [0.7.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.7.0...v0.7.1
528
- [0.7.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.6.2...v0.7.0
529
- [0.6.2]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.6.1...v0.6.2
530
- [0.6.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.6.0...v0.6.1
531
- [0.6.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.5.0...v0.6.0
532
- [0.5.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.4.1...v0.5.0
533
- [0.4.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.4.0...v0.4.1
534
- [0.4.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.3.0...v0.4.0
535
- [0.3.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.2.1...v0.3.0
536
- [0.2.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.2.0...v0.2.1
537
- [0.2.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.1.1...v0.2.0
538
- [0.1.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.1.0...v0.1.1
539
- [0.1.0]: https://github.com/YawLabs/lemonsqueezy-mcp/releases/tag/v0.1.0
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [0.13.2] — 2026-08-23
6
+
7
+ ### Fixed
8
+
9
+ - **The sandbox now grants `LEMONSQUEEZY_TEST_API_KEY`.** `src/secret.ts` reads it, but the `--allow-env` list omitted it, so under `LEMONSQUEEZY_MCP_SANDBOX=1` the variable was **absent** from `process.env` rather than denied -- oam withholds a non-granted variable instead of throwing. The test-mode branch simply took its false path: with both keys set the server silently used the **production** key, and with only the test key set `loadApiKey()` threw `LEMONSQUEEZY_API_KEY or LEMONSQUEEZY_API_KEY_COMMAND environment variable is required` while the key was in fact configured. That silent-misconfiguration shape is the exact hazard the launcher header cites as the reason the sandbox is opt-in rather than default.
10
+
11
+ Scope was judged against the built bundle, not `src/`: `LEMONSQUEEZY_TEST_API_KEY` appears twice in `dist/index.js`, so it ships. `LEMONSQUEEZY_TEST_STORE_ID` appears zero times -- it is read only by `integration.test.ts` -- so it stays ungranted rather than widening the sandbox for a variable the server never reads.
12
+
13
+ ### Added
14
+
15
+ - **`src/sandbox-env.test.ts` fails the build if the allow-list drifts from what the bundle reads.** The gap above existed because nothing compared the two. The test parses the launcher's `--allow-env` array and every `process.env.LEMONSQUEEZY_*` in `dist/index.js`, and asserts the first covers the second; both sides also assert non-empty, so a regex that stops matching after a refactor fails loudly instead of passing vacuously. Asserting against the built artifact rather than sources is deliberate -- that is what runs under the sandbox, and it naturally excludes test-only reads. The inverse direction (granted but never read) is reported as a note rather than asserted, since a grant may legitimately land ahead of the code that uses it.
16
+
17
+ ## [0.13.1] — 2026-08-23
18
+
19
+ ### Changed
20
+
21
+ - **The minimum oam version is now actually enforced.** `oamVersion()` and `atLeast()` were defined but never called, so `OAM_MIN` (`[0, 9, 0]`) was dead code and any oam on the box was spawned regardless of version — including the pre-0.9.0 releases the floor exists to exclude, where `child_process.execFile` ran its arguments through a shell.
22
+
23
+ This server shells out on exactly one path, not on its main ones: `loadApiKey()` splits `LEMONSQUEEZY_API_KEY_COMMAND` with `parseCommand()` and hands the resulting argument vector to `execFileAsync(command, args, { timeout: 10s, maxBuffer: 64 KiB })`. On a pre-0.9.0 oam that carefully-split vector would have been re-split by a shell so the bug the floor guards against is reachable for anyone who configures that variable, and inert for everyone else. The gate itself is not conditional on it: every user sitting on a pre-0.9.0 oam now gets a different runtime.
24
+
25
+ An oam below the floor is refused under `LEMONSQUEEZY_MCP_RUNTIME=oam` and bypassed for Node under `auto`, in both cases naming what it found. The two ways of failing the gate are diagnosed apart: `oamVersion()` returns `null` both when the binary could not be run at all (not executable, wrong arch, a `.cmd`/`.bat` Node refuses, deleted between the stat and the probe) *and* when its `--version` output did not parse. Neither is "too old", so that user is told to check the binary rather than sent after `oam self-update`, the one cause it definitely is not.
26
+
27
+ Cost, stated plainly: the version is read once, only when discovery found something, and it is the launcher's first subprocess — discovery itself remains stat-only. It is paid on every launch that finds an oam, including the ones that go on to fall back to Node.
28
+
29
+ ### Fixed
30
+
31
+ - **The launcher no longer dies with a raw stack trace when `spawn` fails.** Node throws synchronously rather than emitting `error` for some unexecutable targets — notably a `.cmd`/`.bat` on Windows — and the `error` listener is registered *after* the `spawn` call, so it could never observe that throw. Both failure modes now route through one `launchFailed` handler.
32
+ - **Windows `PATH` discovery accepts `oam.exe` only**, instead of walking every `PATHEXT` entry and returning an `oam.cmd` Node cannot execute. Discovery has to agree with execution. A skipped shim is still **named** in the diagnostic, so a `.cmd`/`.bat` shim install no longer reports as "no oam binary was found" a message that reads as "install oam", the one thing that will not help.
33
+ - **A failing in-process fallback no longer escapes as an unhandled rejection.** `void runInProcess()` discarded the promise, and an unhandled rejection is fatal under Node's default handling — so a missing `dist/index.js` replaced the launcher's own diagnostic with the raw stack trace this handling exists to prevent.
34
+ - **Diagnostics that precede `process.exit` are written synchronously.** stderr is async for TTYs and pipes on Windows, so the exit could truncate them. The `errSync` helper additionally loops over short writes (`writeSync` returns a byte count) and swallows the `EAGAIN` macOS raises on a non-blocking piped stderr, giving up quietly rather than crashing a stdio server over a failed diagnostic.
35
+ - **Removed a literal backspace byte (`U+0008`) from the discovery comment.** The backslashes in the documented `%LOCALAPPDATA%\oam\bin` install path had been consumed as escapes — one dropped outright, `\b` becoming a real control character — so the comment naming where the Windows installer puts oam rendered as a corrupted path.
36
+ - **Windows: the launcher no longer hard-kills the server on the first Ctrl-C.** There are no POSIX signals on Windows — `child.kill(sig)` ignores the name and calls `TerminateProcess`, an immediate hard kill (verified: a child with a `SIGTERM` handler never runs it and dies with `code=null`). The launcher forwarded anyway, on the stated assumption that this was a no-op there, so it aborted the graceful shutdown the console's own Ctrl-C had just started and skipped the server's `process.on("exit")` cleanup. The console already delivers the event to the whole process group, so on Windows the launcher now forwards nothing and the escalation timer is the only kill it issues.
37
+ - **A wedged server no longer leaves the launcher hanging.** Forwarding was gated on `child.killed`, which records only that `kill()` was *called* — never that the child is gone — so every signal after the first was swallowed and there was no escape hatch. Escalation is now armed by a timer on the first signal: one press is enough, and a child still alive after a 2s grace window is `SIGKILL`ed and the parent exits `128 + signal`. The timer is cleared when the child exits, so a clean shutdown inside the window still reports the child's own status. Driving it from a timer rather than a signal count also stops the ordinary supervisor sequence (`SIGINT` then `SIGTERM` milliseconds apart) from being misread as impatience and hard-killing a child that is already shutting down cleanly.
38
+ - **`npm test` and `npm run test:integration` are bounded by `--test-timeout=300000`, so a hung test cannot wedge a release.** `node:test` has no default per-test timeout, so a test awaiting an event that never arrives runs forever — and because `npm test` runs unattended inside `release.sh`, that produced a release that never returns rather than one that fails. Deliberately generous: measured files run ~7.5s worst case, so five minutes is roughly 40x headroom and cannot false-fail. Note the semantics — until Node 24 the flag is per-*file*, not per-test ([nodejs/node#57672](https://github.com/nodejs/node/issues/57672)), so the value has to clear the slowest file. Node already exits on the easy case (a pending promise with a drained event loop); what this covers is a hang holding a live handle, such as a child process that never messages back. Dev-side only, and the flag's own Node 20.11.0 floor is already well under the package's `engines: >=22`, so `engines` is untouched.
39
+ - **`release.sh` no longer recommends `npm login --auth-type=web` on an npm auth failure.** The advice made things worse: web login *overwrites* `~/.npmrc`, replacing the automation token with a 2FA-bound web session, so the next attempt `EOTP`s on a WebAuthn challenge instead of succeeding and any CI sharing that token starts failing too — a dead token turned into a harder auth problem. Hit for real while cutting `aws-mcp` v1.8.1. Both sites (the local-run prerequisite block and the non-OTP failure path in the retry loop) now walk the operator through minting a fresh Automation token and writing the registry + `_authToken` lines, and both carry an explicit warning against web login so the old advice is not rediscovered. The failure path also explains that npm answers an unauthorized PUT with **404**, not 401, so "could not be found or you do not have permission" here is almost always an authorization problem rather than a missing package — `npm whoami` tells the two apart. Message and comment text only; no behavior change.
40
+
41
+ ### Security
42
+
43
+ - **`.npmrc` is gitignored, so a project-local npm token cannot be committed.** A project-local `.npmrc` is not something you have to create deliberately — `npm config set --location=project` writes one and some publish tooling drops one in — and it carries a live automation token. Untracked and unignored, a single `git add -A` would publish that token to a public repo. No such file exists here and git has never tracked one; this closes the hole before it opens.
44
+
45
+ ## [0.13.0] 2026-08-08
46
+
47
+ ### Added
48
+
49
+ - **Opt-in `--permission` sandbox on the oam path.** `LEMONSQUEEZY_MCP_SANDBOX=1` — an exact `"1"` compare at `bin/lemonsqueezy-mcp.mjs:157` — makes the launcher prepend oam's process-level flags ahead of the `run` subcommand: `--permission`, `--allow-net=api.lemonsqueezy.com` (plus the hostname parsed out of `LEMONSQUEEZY_SINK_URL` when that is set — it is operator-configured, so it is learned rather than assumed, and a malformed URL adds nothing instead of guessing a host), `--allow-env=` ten of the eleven `LEMONSQUEEZY_*` variables the bundle reads plus `PATH`, and `--allow-child-process` **only** when `LEMONSQUEEZY_API_KEY_COMMAND` is configured. No filesystem grant is issued at all: `src/` opens no files at runtime, and the version read that would have (`createRequire("../package.json")` at `src/index.ts:45`) sits behind a `typeof __VERSION__ !== "undefined"` guard that the `__VERSION__` define in `build.mjs` constant-folds away. Flag ordering is load-bearing: `oam run --permission file.js` is rejected outright with `error: unexpected argument '--permission' found`, which is a good failure only because it is loud.
50
+
51
+ It is opt-in rather than default because a wrong grant does not fail loudly: oam denies a non-granted environment variable by making it **absent** from `process.env` rather than throwing `"NAME" in process.env` is `false`, verified against oam 0.11.0 so an under-granted `LEMONSQUEEZY_API_KEY` reads as "not configured" instead of "denied". Nothing changes on the Node default or the in-process fallback; the flags are assembled only for the `spawn(oam, …)` path at `:220`.
52
+
53
+ Two things to know before turning it on. **The allow-list is one variable short.** `LEMONSQUEEZY_TEST_API_KEY` is not granted, and `src/secret.ts:122` does read it. Under the sandbox the test branch never runs, so its `announceTestModeOnce()` banner never fires and the key silently falls through to the prod key — or, with only the test key set, `loadApiKey()` throws `LEMONSQUEEZY_API_KEY or LEMONSQUEEZY_API_KEY_COMMAND environment variable is required.` Both are exactly the silent-misconfiguration shape the opt-in is hedging against. **And nothing tests any of this.** `sandboxFlags()` has no coverage: no file under `src/` references it or `LEMONSQUEEZY_MCP_SANDBOX`, and this repo has no launcher test.
54
+
55
+ - **`src/release-metadata.test.ts` three assertions that `server.json` and `package.json` cannot drift.** `server.json` is what the Official MCP Registry reads at publish time; it carries the version twice (top-level plus `packages[].version`) and `release.sh` bumps it separately from `package.json`, so an edit that updates one and not the other ships a desynced registry entry — visible to users, invisible to the release. The third assertion covers a different drift mode: `mcpName` in `package.json` must equal `name` in `server.json`, because the registry keys on `name` while npm consumers read `mcpName`, and disagreement puts discovery and install on different identifiers. It asserts both fields are non-empty first, so it cannot pass vacuously if a refactor drops both.
56
+
57
+ Ported from tailscale-mcp, which had carried it alone since May; the same test landed across the sibling servers on the same day, so this is a fleet-wide port rather than a one-off. Its comment records why it was worth copying: it caught a version skew during tailscale's own 0.15.0 release that every other repo would have published silently. Only the generic half is ported — tailscale's README tool-count assertions depend on its own tool-group tables.
58
+
59
+ ### Changed
60
+
61
+ - **The stated minimum oam version is 0.9.0 — declared in this release, but not yet enforced.** `OAM_MIN = [0, 9, 0]`, `oamVersion()` and `atLeast()` all landed in `bin/lemonsqueezy-mcp.mjs` (`:66`, `:120`, `:135`), and the docs describe a floor that holds: README.md:323 and CLAUDE.md:38 both say an old oam falls back to Node with a note on stderr and that `LEMONSQUEEZY_MCP_RUNTIME=oam` turns that into a hard error, with CLAUDE.md going further and calling the floor "enforced in `bin/lemonsqueezy-mcp.mjs`". In the shipped 0.13.0 launcher none of that runs: `OAM_MIN` is never referenced past its declaration, neither helper is ever called, and the spawn path at `:220` invokes only `sandboxFlags()`. Any oam on the box is still spawned regardless of version, and `RUNTIME=oam` against a pre-0.9.0 oam still starts it. The launcher's own header is the one file that does not overstate this — `:48` says plainly that an older oam is *not* an error — but it also omits the `RUNTIME=oam` case the other two describe. The gate is wired up in the following release; treat the floor on 0.13.0 as documentation, not a guard.
62
+
63
+ The floor exists because pre-0.9.0 oam ran `child_process.execFile` arguments through a shell, accepted `exec`'s `timeout` and ignored it, truncated `spawnSync` at `maxBuffer` while reporting success, and treated `stdio: 'inherit'` and `'ignore'` both as `'pipe'`. The first of those is reachable here rather than theoretical: `loadApiKey()` in `src/secret.ts` shells out via `execFile` (`:1`, `:103`) whenever `LEMONSQUEEZY_API_KEY_COMMAND` is set, so the key command's arguments would have been re-split by a shell. That same feature is why the sandbox ties `--allow-child-process` to it instead of granting it unconditionally.
64
+
65
+ - oam re-verified against 0.9.0 in the docs — full MCP handshake, all 64 tools, the `lemonsqueezy://audit-log` resource, working `fetch`, guardrail rejections with error text identical to Node.
66
+
67
+ - The new release-metadata test shipped format-dirty and had to be re-wrapped before `release.sh` would clear its `biome check src/` gate. Whitespace only three `assert` calls line-wrapped, no assertion changed but it blocked the release outright until it landed.
68
+
69
+ ### Documentation
70
+
71
+ - **The cold-start figures are withdrawn in CLAUDE.md and still standing in the README.** CLAUDE.md had justified Node-by-default with 196ms for Node against 424ms for `oam run`; those numbers used the methodology npmjs-mcp later documented as wrong — timing a binary out of a cargo `target/` directory while it was being rebuilt — and interleaved runs against an *installed* oam put it ahead of Node on the sibling servers. Nothing has been re-measured for this repo, so CLAUDE.md now records runtime choice as untimed rather than as a decided cost. **README.md:345 was not touched and still quotes the same 196ms/424ms comparison under "Node stays the default, deliberately", as does the launcher header with its separate npmjs-mcp table** — so the repo currently withdraws the figures in one file and asserts them in two others. Reconciling those is follow-up work, not something this release did. What survives the withdrawal is the reason that never depended on a measurement: oam is not on npm, so the published package requires nothing but Node, and making oam a hard requirement would break `npx` for anyone without it.
72
+
73
+ ## [0.12.0] — 2026-08-07
74
+
75
+ ### Added
76
+ - Runtime launcher at `bin/lemonsqueezy-mcp.mjs`: the published `lemonsqueezy-mcp` command now prefers the [oam](https://oamjs.org) runtime and falls back to Node. `LEMONSQUEEZY_MCP_RUNTIME` selects (`auto` / `oam` / `node`) and `OAM_BIN` overrides discovery. Both paths verified against the MCP surface — handshake plus all 64 tools — and behave identically. The fallback does **not** re-exec Node: npm has already started Node to run the launcher, so it is an in-process `import()` with no extra spawn.
77
+
78
+ ### Changed
79
+ - `.gitignore` excludes `bin/*` rather than `bin/`, so the launcher can be re-included with a negation. A negation cannot undo a directory-level exclusion — that trap shipped a broken `bin` in postgres-mcp, where the launcher was untracked and absent from every fresh clone.
80
+ - `scripts/build-binary.mjs` pins the CLI source entry instead of deriving it from `bin`'s value, which would have resolved to `bin/lemonsqueezy-mcp.ts` once `bin` moved to the launcher — the breakage postgres-mcp shipped in its 0.9.0.
81
+
82
+ All notable changes to `@yawlabs/lemonsqueezy-mcp` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and versioning follows [SEMVER.md](./SEMVER.md).
83
+
84
+ ## [0.11.1] -- 2026-08-07
85
+
86
+ ### Fixed
87
+
88
+ - **A 4xx whose error envelope carries no `detail` no longer dumps raw JSON at the caller.** LemonSqueezy's real 404 is `{"jsonapi":{"version":"1.0"},"errors":[{"status":"404","title":"Not Found"}]}` -- `title`, no `detail`. The message chain read `detail` then the License API's bare `error`, so this shape fell all the way through to the raw response body and an agent asking for a missing store got a JSON blob instead of a reason. `title` is now read between the two.
89
+
90
+ Long-standing, not new in 0.11.0, and invisible to the unit suite because every fixture in it (and every one added during the 0.11.0 audit) used `detail`. Caught by a read-only call against the live API. Both shapes are now pinned, including the precedence when `detail` and `title` are both present.
91
+
92
+ ## [0.11.0] -- 2026-08-07
93
+
94
+ ### Security
95
+
96
+ - **`redactSecrets` no longer degrades to exponential traversal on a shared-reference payload.** The cycle guard tracks the ancestor path (entries removed on the way back up) so a merely *shared* object is redacted normally instead of being falsely reported as `[CIRCULAR]` -- but an ancestor set alone is O(paths), not O(nodes). A "diamond chain" holding two references to the same child at every level has 2^depth paths over depth+1 objects: measured at 3.2 seconds for 23 objects, doubling per level, which at the 32-level cap would block the stdio server for the better part of an hour from inside the audit path. A memo of completed subtrees restores linear behaviour. Reuse is gated on the cached entry having been computed with at least as much depth budget as the current position, so a subtree truncated near the cap is never replayed into a shallower slot where real (possibly secret-bearing) input belongs.
97
+
98
+ ### Added
99
+
100
+ - **`validation_error` log/audit status.** Failure entries previously collapsed client mistakes into `exception`, the same bucket as upstream 5xx and genuine faults. The new `ToolInputError` is tagged `validation_error`, so a log scan at `LEMONSQUEEZY_LOG=error` can separate "an agent sent a malformed request" from "the server or upstream faulted". `guardrail_block` (operator policy refused it) is unchanged.
101
+ - **`preflight` hook on the tool contract.** An input-dependent guardrail that runs ahead of the rate limiters. Both refund tools now declare the cap check there, so a rejected over-cap refund no longer consumes the caller's destructive and `money`-class budgets -- previously a client looping on an over-cap amount could exhaust a `money:2/h` allowance on calls that never left the process.
102
+ - **Cross-store disclosure on `ls_list_stores`.** It has no `storeId` field and no parent ID to scope by, so `LEMONSQUEEZY_ALLOWED_STORE_IDS` does not gate it and it enumerates every store the API key can see. This was documented in the README but absent from the tool description an agent actually reads. `ls_list_affiliates` carried the note already.
103
+
104
+ ### Fixed
105
+
106
+ - **Empty-PATCH guards on the remaining three update tools.** `ls_update_customer`, `ls_update_subscription`, and `ls_update_license_key` sent `attributes: {}` upstream when called with only an ID; `ls_update_webhook` had rejected this locally since 0.10.10. All four now reject before the round-trip and throw `ToolInputError`.
107
+ - **`ls_update_subscription` and `ls_update_customer` descriptions now disclose their conditional-destructive behaviour.** A tool whose destructiveness depends on the input carries `destructiveHint: false`, because the MCP annotation is static and the verdict is per-call -- so an MCP client will not prompt, and the description is the only signal left. `ls_update_subscription` said nothing about pausing or plan switches; `ls_update_customer` mentioned auditing only to point at `ls_archive_customer`, never saying that setting `status: 'archived'` *here* is itself rate-limited and audited. A new invariant in `tools.test.ts` fails the build if any predicate-carrying tool omits the warning.
108
+ - **`Retry-After: " "` no longer means "retry immediately".** `Number("")` is `0`, not `NaN`, so a whitespace-only header fell through the numeric branch and produced a 0 ms wait instead of the 1 s default.
109
+ - **`include: ""` no longer emits a bare `?include=`.** The include schemas set `.max()` but no `.min()`, so an empty string is valid input and split to `[""]`.
110
+ - **Sink response size limit measures UTF-8 bytes, not UTF-16 code units.** `String.length` undercounts every multi-byte character, so a non-ASCII body could exceed the 10 MB limit while reporting a char count under it -- and the message still said "bytes".
111
+ - **A bare `{"error": "..."}` envelope is now read on the management API**, not just the License API, via the shared error handler.
112
+ - **A throwing `isDestructive` predicate can no longer escape as an unhandled rejection.** It is evaluated outside the main try block so it stays in scope for the audit path on the error branch; it now has its own guard that fails closed (treats the call as destructive) and logs the fault.
113
+
114
+ ### Changed
115
+
116
+ - **`apiRequest` and `licenseRequest` share one error path and one 2xx body reader.** Roughly 90 lines of near-identical parse/log/decorate logic collapsed to two helpers. `licenseRequest` still deliberately does not bust the API-key cache on 401, since it authenticates with the caller's license key rather than `LEMONSQUEEZY_API_KEY`.
117
+ - **Both generate-invoice tools share one query builder** (`buildInvoiceQuery`) instead of duplicating an eight-field `URLSearchParams` block.
118
+ - **Cross-store notes are generated from each module's `requiredFilters` array**, so the disclosure and the runtime gate cannot drift apart.
119
+ - **`src/index.ts` is free of top-level await.** The version fallback used `await import("node:module")`, which the CJS single-binary build cannot emit; it survived only because both builds define `__VERSION__` and esbuild constant-folded the branch away. A static `createRequire` import removes the dependency on that folding -- verified by bundling to CJS with the define absent.
120
+
121
+ ### Added
122
+
123
+ - **Opt-in [oam.js](https://oamjs.org) build tooling.** `npm run check:oam` type-checks via `oam check` (tsgo, TypeScript 7 native) -- 2878ms against 4406ms for `tsc --noEmit`, same clean result. `npm run build:binary:oam` builds the standalone binary via `oam compile` instead of the Node SEA path, writing to the same `bin/<platform>-<arch>/` location so the release staging script consumes either unchanged. Neither touches the published npm package.
124
+
125
+ **Node remains the default runtime.** oam runs the server unmodified (verified against 0.8.2: full handshake, all 64 tools, the audit-log resource, working `fetch`, guardrail errors identical to Node), but cold start measured 196ms for Node against 424ms for `oam run` -- and since an MCP client pays startup once per session, that is the cost that matters. oam is also not distributed on npm, so defaulting to it would break `npx` for anyone without it. `src/` stays runtime-agnostic (no `oam:` imports, tests on `node:test`) so the Node path remains a real fallback rather than a nominal one.
126
+
127
+ - `oam/` added to `.gitignore` -- any `oam` invocation writes a bytecode cache there.
128
+
129
+ ### Documentation
130
+
131
+ - `0` documented as a valid value for the refund cap and both rate limits (blocks everything; unset/empty means no limit).
132
+ - `LEMONSQUEEZY_LOG` row now lists what each failure `status` means.
133
+ - README's allowlist row no longer describes `ls_list_stores` as routing "by its own resource ID"; the two genuinely ungated list tools are called out separately from the ID-targeted ones.
134
+
135
+ ## [0.10.13] -- 2026-06-11
136
+
137
+ ### Added
138
+
139
+ - **Cross-platform single-binary pipeline** (Scoop + Homebrew) via `scripts/build-binary.mjs` and `scripts/stage-release-asset.mjs`.
140
+
141
+ ### Fixed
142
+
143
+ - **`npm test` runs every test file under POSIX sh**, not only nested ones -- an unquoted `**/*` glob skipped the top-level files.
144
+
145
+ ### Changed
146
+
147
+ - Node engine requirement raised to `>=22`; dev-dependency vulnerabilities cleared.
148
+
149
+ ## [0.10.12] -- 2026-06-02
150
+
151
+ ### Fixed
152
+
153
+ - **Release drift guard compares tag-object SHAs**, so resuming a partial release no longer false-aborts.
154
+ - **`SKIP_LINT=1` escape hatch** for environments where the lint runner itself is broken.
155
+ - **`release.sh` refuses to push if origin's tag drifted from local.**
156
+ - README "Add to Yaw MCP" badge points at the https forwarder.
157
+
158
+ ### Added
159
+
160
+ - End-to-end coverage for the refund-cap guard on both refund tools.
161
+
162
+ ## [0.10.11] -- 2026-05-28
163
+
164
+ ### Changed
165
+
166
+ - **MCP Registry publish folded into `release.sh`**; `release.yml` and the remaining non-release workflows removed. `release.sh` is now the only release path.
167
+
168
+ ### Fixed
169
+
170
+ - Confirmation prompt is tty-gated, so non-interactive runs no longer block.
171
+ - `server.json` syncs unconditionally rather than only inside the version-bump branch.
172
+ - Falls back to the `gh` auth token for MCP Registry authentication.
173
+
174
+ ## [0.10.10] -- 2026-05-22
175
+
176
+ ### Fixed
177
+
178
+ - **`release.sh` no longer demands a workstation npm session for CI-publish flows.** The pre-flight `npm whoami` gate was a vestige of the local-only-deploy era (added in `53de878`); when CI publishing was restored in `e3ef87a` the gate was kept but the publish itself moved to CI's org-level `NPM_TOKEN`, so the workstation never uses that credential. Blocked the v0.10.10 release attempt with "npm is not authenticated" despite the intended publish path not needing it. Mirrors `tailscale-mcp/release.sh`, which never had the gate.
179
+
180
+ ### Security
181
+
182
+ - **Webhook `url` restricted to http/https schemes.** `ls_create_webhook` and `ls_update_webhook` previously accepted any string up to 10k chars; `z.string().url()` alone would have additionally accepted every URL-parseable scheme (`mailto:`, `file:`, `javascript:`, `ftp:`, `chrome-extension:`, ...). Anything non-http(s) stored as a webhook target is unreachable in the best case and an injection sink in the worst. The new `httpsUrlSchema` chains `.url()` with a `.refine()` enforcing `^https?://`, applied on both tools.
183
+ - **`ls_update_webhook` secret rotation gated as destructive.** Rotating the signing secret silently breaks signature verification on every receiver until they redeploy with the new value. The tool now declares an `isDestructive` predicate flagging any `secret` change, routing those calls through `checkDestructiveRateLimit` + audit logging via the wrapper's existing duck-typed integration. URL and events changes stay on the regular path.
184
+
185
+ ### Changed
186
+
187
+ - **`ls_update_webhook` rejects empty PATCH locally.** Calling with only `webhookId` and no fields to change previously sent an empty `attributes: {}` PATCH that the API would 422 with a less clear message. The handler now throws `"ls_update_webhook requires at least one of: url, events, secret"` before the round-trip.
188
+ - **`ls_update_webhook` description warns about secret rotation.** The tool-level description now calls out that setting `secret` is destructive (rate-limited and audited) so LLM callers see the consequence before invoking.
189
+
190
+ ### Tests
191
+
192
+ - **Regression coverage for the `events.min(1)` fix** (`f7374bd`): schema-rejection tests on both `ls_create_webhook` and `ls_update_webhook` lock in the empty-array rejection.
193
+ - **URL validation coverage**: non-URL string rejection on both tools, four non-http(s) schemes (`mailto:`, `file:`, `javascript:`, `ftp:`) rejected on both tools, http and https explicit acceptance.
194
+ - **`ls_update_webhook` predicate tests** mirror the `ls_update_license_key` shape: secret-only as destructive, url-only and events-only as non-destructive, empty input as non-destructive.
195
+ - **`ls_update_webhook` URL-encoding** test added in parallel to the existing `ls_delete_webhook` coverage.
196
+ - **Handler-side empty-PATCH rejection** test asserts the new error message.
197
+
198
+ ## [0.10.9] -- 2026-05-22
199
+
200
+ ### Changed
201
+
202
+ - **Webhook `events` array now requires at least one entry.** `ls_create_webhook` and `ls_update_webhook` previously let an empty array pass local Zod validation, so a no-op webhook configuration only failed at the LemonSqueezy API as a 422. The new `.min(1)` makes the rejection local with a clearer message; on update, the field stays optional but, when supplied, must be non-empty.
203
+
204
+ ## [0.10.8] -- 2026-05-19
205
+
206
+ ### Security
207
+
208
+ - **Sink response body size guard extended to the error branch.** `sinkRequest` in `src/tools/sink.ts` previously only size-checked 2xx response bodies; a misbehaving sink returning a giant 4xx/5xx body could still buffer the whole thing into memory before any limit fired. A new `readBodyOrSizeError` helper pre-checks `Content-Length` against the 10 MB cap and is applied to both the error and success branches. The post-read length check on the 2xx path is retained as belt-and-braces against a lying `Content-Length`.
209
+
210
+ ### Changed
211
+
212
+ - **2xx body-read mid-stream failures now collapse to the uniform `{ ok: false, error }` shape.** Previously a socket reset partway through reading the body propagated as an exception out of `sinkRequest`, surfacing as a less-informative error via the wrapper's catch-all. The 2xx body read is now wrapped in a try/catch that returns `Sink response body read failed: <message>` -- consistent with every other failure mode in the function.
213
+ - **`src/secret.ts` cache-hit branch tightened.** The test-mode cache-hit branch previously called `announceTestModeOnce()` redundantly (the flag is set on the first miss, so the cache-hit call was dead). Removed the call and the now-obsolete defensive comment.
214
+
215
+ ### Tests
216
+
217
+ - **Sink coverage expanded** to pin `authorityClass` per tool (`read` for `ls_sink_events_list` / `ls_sink_stats`, `mutate` for `ls_sink_event_mark_processed`), 4xx-oversized-Content-Length, 2xx-lying-Content-Length, and 2xx mid-stream body-read failures. `stubFetch` gained an optional `responseHeaders` field so error-branch tests no longer override `globalThis.fetch` inline.
218
+ - **`parseCommand` contract pinned** in `src/secret.test.ts` for the four tokenizer edges that the rest of the suite only touched by accident: unterminated quote, all-quotes-collapse-to-empty, quoted-args-with-spaces (both quote styles), and quote-then-bare-word concatenation. Also added a 64 KB `maxBuffer` overflow test that exercises the `execFile` `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` rewrap path.
219
+
220
+ ## [0.10.7] -- 2026-05-16
221
+
222
+ ### Security
223
+
224
+ - **Allowlist-bypass surface closed at the test layer.** `checkStoreScopedToolInput` in `src/wrapper.ts` enforces `LEMONSQUEEZY_ALLOWED_STORE_IDS` by reading the literal input field name `storeId` from the tool's Zod shape. Today every list tool that filters by store happens to use that name, but the convention wasn't asserted anywhere -- a future tool whose filterMap mapped a differently-named input (e.g. `store`) to `filter[store_id]` would silently bypass the allowlist. `src/api.ts` `listHandler` now exposes its `filterMap` on the returned handler, and `src/tools/tools.test.ts` adds an invariant that fails CI for any drift between the filter key (`store_id`) and the input field name (`storeId`).
225
+
226
+ ### Changed
227
+
228
+ - **`wrapper.ts` skips the success/error entry literal when no consumer wants it.** A non-destructive read at `LEMONSQUEEZY_LOG` unset (i.e. "off") previously built a full audit entry on every call, then `logEvent` discarded it inside its level check. Now the wrapper consults `wouldLogToolCall({ isDestructive, isError })` -- a new export from `src/logger.ts` -- before allocating. Destructive calls always build the entry because the audit-buffer push is independent of the log level. No observable behavior change; saves a small object literal per non-destructive read on the most common configuration.
229
+
230
+ ## [0.10.6] -- 2026-05-16
231
+
232
+ ### Security
233
+
234
+ - **Sink response body size cap.** `sinkRequest` in `src/tools/sink.ts` now rejects responses larger than 10 MB before calling `JSON.parse`, defending the in-process MCP server against CPU exhaustion from a malicious or misconfigured sink (`Sink response body too large: X bytes exceeds Y byte limit`). The 10 s `AbortSignal` timeout already bounded wall-clock; the size cap bounds the parse cost as well.
235
+
236
+ ### Changed
237
+
238
+ - **`buildQuery` page-param values are URL-encoded.** Brought `page[number]` / `page[size]` in line with the `include` / `filter` branches (which already wrapped values in `encodeURIComponent`). Today the Zod schemas constrain both to integers so the encoding is a no-op, but if pagination ever widens to accept a string cursor the values are already safely escaped.
239
+ - **`smithery.yaml` numeric fields are typed `integer`.** `lemonsqueezyMaxRefundAmountCents` and `lemonsqueezyDestructiveRateLimit` now declare `type: integer, minimum: 1` (were `type: string`). Smithery's config UI now renders a number input with client-side validation. The `commandFunction` `String(...)` coerces back to env-var form, and the truthy check switched to `!= null` so the (admittedly nonsensical) value `0` would not be silently dropped.
240
+
241
+ ### Docs
242
+
243
+ - **`CHANGELOG.md` em-dashes normalized to ASCII `--`** throughout v0.9.3 and earlier sections. Brings older entries in line with the v0.10.x style and avoids the Windows-terminal mojibake risk called out in the Yaw Mode discipline.
244
+
245
+ ## [0.10.5] -- 2026-05-16
246
+
247
+ ### Changed
248
+
249
+ - **`release.sh` step 3 now bumps `server.json` alongside `package.json` and stages it in the release commit.** Previously `server.json` was treated as a derived artifact only -- `release.yml` ran a `jq` step at publish time to sync `version` and `packages[0].version` from `$GITHUB_REF_NAME`, but the committed file was never updated, so a manual `mcp-publisher publish` outside CI would push a stale registry version. The new `node -e` step mirrors the CI `jq` so the committed source matches the latest published version on every release. No `jq` dependency added (`node` was already a release-flow prereq).
250
+ - **`server.json` resynced to 0.10.4** to clear the existing drift (file was last touched at 0.10.0; releases 0.10.1 through 0.10.4 all relied on CI's publish-time `jq` rewrite). 0.10.5 going forward, the workstation release path keeps it current automatically.
251
+
252
+ ### Docs
253
+
254
+ - **CHANGELOG footers backfilled** for v0.10.0 through v0.10.4. Adds the missing `[X.Y.Z]: .../compare/...` lines per Keep a Changelog and points `[Unreleased]` at `v0.10.4...HEAD` (previously stuck at `v0.9.3...HEAD`).
255
+
256
+ ## [0.10.4] -- 2026-05-16
257
+
258
+ ### Changed
259
+
260
+ - **GitHub release notes now come from `CHANGELOG.md`, not just commit subjects.** `release.sh` step 6 extracts the section between `## [X.Y.Z] -- ...` and the next `## [` and passes that body to `gh release create --notes`. Falls back to the previous `git log --oneline` behavior if no matching section is present (or the section is whitespace-only), then to "Initial release". Removes the long-standing pattern of single-line "- vX.Y.Z" release bodies on the GitHub releases page (every 0.10.x release until this one).
261
+
262
+ ## [0.10.3] -- 2026-05-16
263
+
264
+ ### Fixed
265
+
266
+ - **Startup env-var parse errors now print a single line, not a stack trace.** The 0.10.1 eager-parse of `LEMONSQUEEZY_DISABLE_CLASSES` / `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS` / `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` threw an uncaught `Error`, so operators saw Node's stack dump before the helpful message. `src/index.ts` now wraps the call in try/catch and exits 1 with just the error message -- e.g. `LEMONSQUEEZY_DISABLE_CLASSES contains unknown class "mony" (expected one of: read, pii, mutate, money, recurring, key, webhook)`.
267
+
268
+ ## [0.10.2] -- 2026-05-16
269
+
270
+ ### Changed
271
+
272
+ - **`CHANGELOG.md` is now included in the published npm tarball.** Added it to `package.json`'s `files` array. npm auto-includes `README` and `LICENSE` regardless of the allowlist, but `CHANGELOG.md` is not in that auto-include set -- so prior versions shipped without it, and an operator inspecting `node_modules/@yawlabs/lemonsqueezy-mcp` had no in-package changelog. The README and bundle are unchanged; tarball size grows by ~29 kB.
273
+
274
+ ## [0.10.1] -- 2026-05-16
275
+
276
+ ### Fixed
277
+
278
+ - **`smithery.yaml` now exposes the sink-bridge env vars.** Added `lemonsqueezySinkUrl` and `lemonsqueezySinkAdminToken` to `configSchema.properties` and forwarded them in `commandFunction`. Without this, Smithery-installed instances could not configure the 0.10.0 `ls_sink_*` tools and they always returned "not configured".
279
+ - **Guardrail env vars are now parsed at server boot, not first call.** A new `loadGuardrailOptions()` export is invoked from `src/index.ts` before stdio connect, so a typo'd `LEMONSQUEEZY_DISABLE_CLASSES`, malformed `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS`, or non-numeric `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` crashes the process at startup. Previously the parse was lazy and a misconfigured deployment booted cleanly, passing liveness probes, and only surfaced the error on the first tool invocation.
280
+
281
+ ## [0.10.0] -- 2026-05-15
282
+
283
+ ### Added
284
+
285
+ - **Webhook-sink bridge tools** for the optional [@yawlabs/lemonsqueezy-webhook-sink](https://github.com/YawLabs/lemonsqueezy-webhook-sink) process. Three new tools give an agent a unified read surface for "what webhooks have actually fired" alongside the existing management-API reads:
286
+ - `ls_sink_events_list` -- list received webhook events with optional `since` / `type` / `limit` filters
287
+ - `ls_sink_event_mark_processed` -- idempotent ack from your consumer
288
+ - `ls_sink_stats` -- total, unprocessed, last-received timestamp
289
+ - `LEMONSQUEEZY_SINK_URL` and `LEMONSQUEEZY_SINK_ADMIN_TOKEN` env vars to configure the bridge. Tools are always registered for `tools/list` discovery; missing env vars surface at call time as a structured "not configured" error (with a pointer to the sink repo), not a registration-time failure. 10s fetch timeout via `AbortSignal.timeout()`; auth failures (401), admin-disabled (404), and transport errors each get a tailored diagnostic.
290
+
291
+ ## [0.9.3] -- 2026-05-15
292
+
293
+ ### Fixed
294
+
295
+ - `server.json` rewritten against the current registry schema (`2025-12-11`). The 0.9.2 release's MCP-Registry publish step failed validation against the live schema: `description` exceeded the 100-char cap, the package field was `registry_type` (snake_case) where the schema expects `registryType` (camelCase), and the required `packages[].transport` field was missing. 0.9.2 is live on npm and GitHub but did not reach the Official MCP Registry. 0.9.3 is the first release that should land on `registry.modelcontextprotocol.io`.
296
+
297
+ ## [0.9.2] -- 2026-05-15
298
+
299
+ ### Added
300
+
301
+ - `.github/workflows/release.yml` now publishes to the [Official MCP Registry](https://registry.modelcontextprotocol.io) after the post-publish smoke test passes. Authentication is via GitHub OIDC -- the `id-token: write` permission already enabled for npm provenance also satisfies the registry's auth; no `MCP_*` secret is required. The namespace `io.github.YawLabs/*` is authorized purely from the OIDC `repository_owner` claim, so anyone outside the YawLabs GitHub org cannot publish under it. A `jq` step overwrites `server.json`'s `version` (and `packages[0].version`) from the pushed tag, so a forgotten manual bump on `server.json` no longer publishes a stale version to the registry. Downstream registries (Glama, PulseMCP, mcpservers.org) that auto-source from the official registry now pick up each `@yawlabs/lemonsqueezy-mcp` release without a manual mcp-publisher run.
302
+
303
+ ### Docs
304
+
305
+ - README Resources section now spells out the full redaction policy (every secret-named key the regex matches, plus JWT-shaped string values), so an operator who sees a `[REDACTED]` under an innocuous key knows where it came from.
306
+ - README Development section documents `npm run gen:containerfile` / `npm run check:containerfile` so a contributor editing `Dockerfile` knows to regenerate.
307
+ - README Releasing section notes that the local `./release.sh` path does not publish to the Official MCP Registry (CI-only) and gives the manual `mcp-publisher` commands as the fallback when a release is cut without CI.
308
+
309
+ ## [0.9.1] -- 2026-05-15
310
+
311
+ ### Docs
312
+
313
+ - README "Features" list now mentions authority-class disable, per-class rate limits, and the audit-log MCP Resource -- previously omitted under the umbrella "Guardrails" bullet despite being documented in detail later in the file.
314
+ - README description of `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` now lists every destructive trigger the runtime classifier counts: `ls_update_license_key` calls that change `activationLimit` (added in 0.7.1), `ls_update_customer` calls with `status: "archived"`, plus the previously-documented pause/plan-switch and `disabled: true` paths.
315
+ - README qualifies the "issue a scoped LemonSqueezy API key" recommendation. LS API keys are account-wide; the qualifier directs readers to LS team-membership scoping where it exists and treats the env-var guardrails as the primary control surface for what LS itself can't express (per-class rate ceilings).
316
+
317
+ ### Changed
318
+
319
+ - `audit-buffer.ts` overflow path is now O(1) (circular index) instead of O(N) (`Array.splice(0, …)`). No observable behavior change; the buffer caps at 1000 entries and presents the same most-recent-first read order.
320
+ - `src/index.ts` split -- the registered MCP tool wrapper and the audit-log Resource read-callback now live in `src/wrapper.ts` so unit tests can exercise them end-to-end without starting the stdio MCP server. `src/index.ts` becomes a thin entry point that imports `createToolHandler` and `readAuditLogResource` from the new module. No observable behavior change in the published binary.
321
+ - `secret.ts` cache fingerprint no longer embeds the raw API key. The fingerprint is now a SHA-256 digest of `mode:value`, preserving the mid-process change-detection semantics while removing the only in-memory copy of the key outside the `cached.key` field itself.
322
+ - `redact.ts` now also masks any string value matching the LemonSqueezy bearer-token shape (`eyJ…`-style JWT prefix), even when the key name is innocuous. Closes the future-feature gap where a destructive tool accepts a `customData`/`metadata` object whose values happen to contain a key. Also covers additional credential / PII key names (`private_key`, `pin`, `ssn`, `social_security_number`, `credit_card`, `card_number`, `cvv`, `cvc`).
323
+
324
+ ### Fixed
325
+
326
+ - Smithery one-click install now exposes `LEMONSQUEEZY_DISABLE_CLASSES` and `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS` -- the two env vars added in 0.9.0. Previously a Smithery user had to hand-edit the spawned process env to reach the authority-class layer.
327
+ - Smithery description for `lemonsqueezyDestructiveRateLimit` now states the actual format (a positive integer interpreted as calls per 60-second rolling window) instead of "Format depends on server defaults."
328
+ - `logger.ts` JSON-stringify fallback now coerces `entry.error` to a plain string before the second serialization pass, so a non-string error field (an `Error` with a circular `cause` chain, or an object with a throwing `toJSON`) no longer skips both write attempts and degrades to silence. Tests cover the case.
329
+ - `integration.test.ts` `before()` hook now clears `LEMONSQUEEZY_API_KEY_COMMAND` and `LEMONSQUEEZY_TEST_API_KEY` before setting `LEMONSQUEEZY_API_KEY` from the test key, and resets the secret cache. Previously a developer with a vault-backed `LEMONSQUEEZY_API_KEY_COMMAND` would silently run the integration suite against their dev key, because the command takes precedence over the bare env var in `secret.ts`.
330
+ - `Containerfile` is now generated from `Dockerfile` via `scripts/sync-containerfile.mjs`. `npm run check:containerfile` (called by `release.sh` and CI) verifies the two stay in sync; `npm run gen:containerfile` regenerates. Removes the byte-identical-but-not-actually drift surface where editing one file silently left the other behind.
331
+
332
+ ### Added
333
+
334
+ - `src/wrapper.ts` -- extracted production module containing `createToolHandler` (the MCP tool registration wrapper) and `readAuditLogResource` (the audit-log Resource read-callback). Same logic that previously lived inline in `src/index.ts`; now reusable and testable.
335
+ - `src/wrapper.test.ts` -- end-to-end test that exercises the registered MCP tool wrapper from `src/wrapper.ts`. Covers the order in which `checkClassAllowed` / `checkClassRateLimit` / `checkDestructiveRateLimit` / `checkStoreScopedToolInput` fire, the destructive-call audit pipeline (logger + ring buffer), the audit-log MCP Resource (`lemonsqueezy://audit-log`), and the predicate-destructive routing (e.g. `ls_update_license_key` with `activationLimit`). Previously every handler test invoked `tool.handler(input)` directly and the wrapper was untested.
336
+
337
+ ## [0.9.0] -- 2026-05-14
338
+
339
+ ### Added
340
+
341
+ - **Authority classes.** Every tool now declares an `authorityClass` describing the kind of business authority a caller needs to invoke it (`read`, `pii`, `mutate`, `money`, `recurring`, `key`, `webhook`). The class is separate from the existing destructive/read-only annotation.
342
+ - `LEMONSQUEEZY_DISABLE_CLASSES` -- comma-separated list of classes to refuse outright. Example: `money,recurring,pii` lets an agent run reads but blocks refunds, subscription changes, and customer-record access. Unknown class names throw at server startup.
343
+ - `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS` -- per-class rolling rate limits with a DSL. Each entry is `class:N`, `class:N/m`, or `class:N/h`. Example: `money:2/h,recurring:5/h,key:10/m`. Composes with `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` -- both must pass. No LemonSqueezy permission can express "max 2 refunds per hour"; this is the only place that policy can live.
344
+ - `deprecate.yml` workflow -- CI-driven `npm deprecate` via `workflow_dispatch`, using the org-level `NPM_TOKEN` and the same `release-npm` concurrency group as `release.yml`. No local WebAuthn session needed for a deprecate run.
345
+ - `server.json` + `mcpName` field in `package.json` -- prepares the repo for the Official MCP Registry. Downstream registries (Glama, PulseMCP, mcpservers.org) auto-source from the official registry, so a single publish reaches the ecosystem.
346
+ - README "Add to mcp.hosting" install button -- one-click sync of the server into [mcp.hosting](https://mcp.hosting) so it propagates to every MCP client a user has configured.
347
+
348
+ ### Changed
349
+
350
+ - README "Authority classes" section reframed to lead with the authoritative control (a scoped LemonSqueezy API key) and present the env vars as defense in depth. `RATE_LIMIT_PER_CLASS` is called out as load-bearing (no LS access-control equivalent); `DISABLE_CLASSES` is positioned as a fast deploy-time alternative when the cost of a key rotation outweighs the strength gained.
351
+ - `release.yml` smoke test now retries the actual `npx --version` call (30 × 10s) instead of gating on `npm view`. The two paths hit different CDN caches; `npm view` can clear while `npx` still ETARGETs on a stale mirror. Mirrors the pattern in `aws-mcp` / `tailscale-mcp`.
352
+
353
+ ## [0.8.1] -- 2026-05-13
354
+
355
+ ### Fixed
356
+
357
+ - `handlers.test.ts` isolation: CI's integration job sets `LEMONSQUEEZY_TEST_API_KEY` from a repo secret. With the priority chain introduced in 0.8.0 (`COMMAND` > `TEST_API_KEY` > `API_KEY`), that injected value took precedence over the in-test stub and broke three handler tests (`ls_get_user` bearer-token assertion plus the missing/empty-key cases). The suite-level `before()` now saves and clears all three source env vars, restores them in `after()`, and resets the secret cache. Reproducible locally by running with `LEMONSQUEEZY_TEST_API_KEY=ci-stub-key LEMONSQUEEZY_API_KEY="" npm test`.
358
+
359
+ ## [0.8.0] -- 2026-05-13
360
+
361
+ Distribution-readiness pass -- closes feature gaps against other OSS LemonSqueezy MCP servers ahead of broader distribution.
362
+
363
+ ### Added
364
+
365
+ - **Smithery one-click install.** `smithery.yaml` declares the stdio start command + JSON Schema for env vars; Smithery generates the config UX. Install via `npx -y @smithery/cli install @yawlabs/lemonsqueezy-mcp --client claude`.
366
+ - **Container images.** `Dockerfile` + `Containerfile` + `.dockerignore`. Multi-stage build on `node:20-alpine`, runs as the non-root `node` user, copies only `dist/index.js` into the runtime image. Stdio transport; no port exposed. Pass env via `-e LEMONSQUEEZY_*`.
367
+ - `LEMONSQUEEZY_TEST_API_KEY` -- separate test-mode key. Priority sits between `LEMONSQUEEZY_API_KEY_COMMAND` (highest) and `LEMONSQUEEZY_API_KEY` (lowest), so a developer can point the server at a sandbox store without unsetting their production key. A one-shot stderr `test_mode` notice fires on first activation per process.
368
+ - **MCP Resource `lemonsqueezy://audit-log`** -- bounded ring buffer (last 1000 entries, most-recent-first) of destructive-call audit entries, exposed as `application/x-ndjson`. Lets clients without stderr access retrieve the audit trail structurally. Secret-shaped input fields are already redacted before they reach the buffer.
369
+
370
+ ### Changed
371
+
372
+ - Dev-dependency audit warnings cleared via `npm audit fix`. Runtime bundle is unaffected (zero runtime deps).
373
+
374
+ ## [0.7.1] -- 2026-05-13
375
+
376
+ Hardening pass on the v0.4.0 guardrails after observing realistic agent failure modes.
377
+
378
+ ### Added
379
+
380
+ - **Retry deadline.** `OVERALL_DEADLINE_MS=90s` ceiling on total wall clock across retries. The timeout error reports actual elapsed and attempt count (`Request timed out after Xs (N attempts)`) instead of the per-attempt budget. The loop stops on 5xx/429/timeout once the deadline is reached rather than starting a new attempt or sleeping past it.
381
+ - **Audit redaction (`src/redact.ts`).** Masks values of secret-shaped keys (`secret`, `password`, `token`, `api[_-]?key`, `bearer`, `authorization`, `signing[_-]?secret` -- case-insensitive, whole-word) before the entry reaches the audit log. Anchored regex so business identifiers (`licenseKey`, `instanceId`, `storeId`) are preserved. Cycle-safe via `WeakSet` with a 32-depth cap. Defense in depth -- no destructive tool today carries a secret-typed input, but flipping any webhook tool to destructive would otherwise leak its signing secret.
382
+ - **`requiredFilters` scoping.** When `LEMONSQUEEZY_ALLOWED_STORE_IDS` is active, list tools that lack a `storeId` field must specify at least one parent filter (orderId, productId, variantId, etc.). Closes the silent-no-op gap on `ls_list_files` / `ls_list_prices` / `ls_list_variants` / `ls_list_order_items` / `ls_list_subscription_items` / `ls_list_usage_records` / `ls_list_discount_redemptions` / `ls_list_license_key_instances`. Each affected tool description flags the cross-store consideration. `ls_list_affiliates` has no meaningful parent filter and is description-only.
383
+
384
+ ### Changed
385
+
386
+ - `ls_update_license_key` is now classified as destructive whenever `activationLimit` changes, not just when `disabled: true`. Shrinking the limit can revoke access and the input alone doesn't reveal shrink-vs-grow direction.
387
+ - `ls_create_checkout` email validation tightened to `.email().max(320)` to match `ls_create_customer` (was `max(10000)` with no shape check).
388
+
389
+ ## [0.7.0] -- 2026-05-06
390
+
391
+ ### Added
392
+
393
+ - `LEMONSQUEEZY_LOG=audit|error|all` -- fine-grained log levels for long-running deployments. `audit` keeps destructive-call entries plus errors and drops successful reads (recommended for production where log volume matters over weeks). `error` keeps only failures. `all` is the verbose default. `json` is retained as a backwards-compat alias for `all`.
394
+
395
+ ### Changed
396
+
397
+ - Tool ID validation now uses a shared `lsIdSchema` (numeric string regex) so a typo fails at the schema layer with a clear message instead of as an opaque 422 from the upstream API.
398
+ - `ls_update_customer.status` tightened to `z.literal("archived")` so the destructive predicate cannot be side-channeled by an unrecognized status string.
399
+ - Secret cache refactored to a fingerprint-keyed entry. `invalidateApiKeyCache()` (called from `api.ts` on 401/403 responses) now behaves uniformly across the env, test, and command source modes. A rotated upstream key picks up on the next request without waiting on the 1h TTL.
400
+ - Integration-test unique suffix switched from a timestamp slice to `crypto.randomUUID().slice(0, 8)`. Two overlapping CI runs (nightly + `workflow_dispatch`) that started in the same millisecond no longer collide on resource naming.
401
+
402
+ ### Fixed
403
+
404
+ - `release.sh` annotated tags (`git tag -a`) -- `git push --follow-tags` silently skips lightweight tags. Pre-0.7.0, the release commit could push without the tag, leaving CI release unfired. Caught by review before any failed run.
405
+ - `release.sh` idempotency check now queries `@yawlabs/<pkg>@${VERSION}` specifically rather than the package's `latest` dist-tag. The bare query returned whichever version is latest on the registry, so an out-of-band higher version made the script try to re-publish the current one and fail with "cannot publish over previously published version." The versioned form returns the version when it exists and empty otherwise -- correct idempotency semantics.
406
+ - `release.sh` push now uses `--follow-tags` instead of `--tags` so stale local tags don't ride along (`--tags` pushes every local tag).
407
+ - `release.yml` concurrency group is now a literal `release-npm` (workflow-scoped); previously `release-${{ github.ref }}` evaluated to per-tag groups and back-to-back tag pushes would race on `npm publish`.
408
+ - `ci.yml` no longer runs a redundant explicit `npm run build` step -- `npm test` is `npm run build && node --test dist/...` for this dist-based repo, so the explicit step was building twice per matrix cell for zero signal.
409
+
410
+ ### Restored
411
+
412
+ - `CODEOWNERS` and `dependabot.yml` (dropped along with the workflows in 0.6.0; restored when the workflows came back in 0.6.1). Routes review requests to `@jeffyaw` and bumps npm deps weekly / github-actions deps monthly.
413
+
414
+ ## [0.6.2] -- 2026-05-04
415
+
416
+ ### Changed
417
+
418
+ - `release.yml` now runs a post-publish smoke test that fetches the just-published tarball via `npx -y @yawlabs/lemonsqueezy-mcp@<version> --version` and asserts the binary executes and prints the expected version. Catches packaging regressions (missing bin shebang, broken `files` entry, bad esbuild output) before they reach real users.
419
+ - `release.sh` step 7 now verifies that CI publishes carry a sigstore provenance attestation. A missing attestation in CI mode is a soft warning; local publishes legitimately skip provenance.
420
+
421
+ ## [0.6.1] -- 2026-05-04
422
+
423
+ ### Changed
424
+
425
+ - Restored CI release plumbing (`ci.yml`, `integration.yml`, `release.yml`) ported from `tailscale-mcp`. Tag-and-let-CI is the preferred release path; local `release.sh <version>` still works for end-to-end runs from the workstation.
426
+ - `release.sh` gained a CI mode that derives version from `$GITHUB_REF_NAME`, skips local-only gates, and publishes with `--provenance`.
427
+
428
+ ### Fixed
429
+
430
+ - `integration.yml` now requires both `LEMONSQUEEZY_TEST_API_KEY` and `LEMONSQUEEZY_TEST_STORE_ID` together. A half-configured repo previously ran the workflow successfully with every integration suite silently skipped, masking zero coverage as green CI.
431
+ - `release.sh` CI mode now hard-fails when `package.json` disagrees with the pushed tag instead of bumping inside the ephemeral checkout, which would have published the right version while leaving `main` pointing at the old one.
432
+ - `release.sh` `npm publish` retry only fires on EOTP/EAUTH/OTP messages. Other failures (duplicate-version E403, packaging errors) bail immediately instead of wasting 60s in the retry loop.
433
+ - `package.json` `prepublishOnly` trimmed to `npm run build`. `release.sh` already runs lint + tests before publishing in both modes, so the embedded test run was doubling the CI work per release.
434
+
435
+ ## [0.6.0] -- 2026-04-24
436
+
437
+ ### Changed
438
+
439
+ - **Breaking.** `LEMONSQUEEZY_ALLOWED_STORE_IDS` now requires `storeId` on list tools that accept it as a filter (`ls_list_orders`, `ls_list_subscriptions`, etc.). Previously the allowlist was silently bypassed when callers omitted the optional filter, returning data from every store the API key could see. Callers that relied on the unfiltered behavior must now pass an allowed `storeId` explicitly.
440
+ - `ls_update_license_key` calls that set `disabled: true` are now classified as destructive at runtime, so revocations engage `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` and the audit log. Benign edits (expiry, activation limit) stay on the regular path.
441
+ - `ls_update_subscription` rejects non-numeric `variantId` at the schema level instead of producing `NaN` upstream.
442
+ - `release.sh` rewritten as a local-only, idempotent deploy with branch and auth pre-flight, EOTP retry on `npm publish`, and clearer failure messages. Re-running with the same version after a partial failure resumes from where it stopped.
443
+
444
+ ### Fixed
445
+
446
+ - Path segments are URL-encoded across `getHandler` and every inline tool handler via a new `encodePath()` helper, closing a path-injection surface where IDs containing `/` could target adjacent endpoints.
447
+ - Logger emits a degraded fallback entry on `JSON.stringify` failure so destructive-call audit trails survive circular inputs.
448
+ - Checkout `billing_address` composition is now order-independent.
449
+ - Integration test env mutation moved from module-eval to a `before()` hook.
450
+
451
+ ### Removed
452
+
453
+ - All of `.github/` (workflows, dependabot, CODEOWNERS). There is no CI -- `release.sh` is the only supported release path.
454
+ - `test:ci` npm script.
455
+
456
+ ### Docs
457
+
458
+ - README documents the local release flow and the one-time `npm login` / `gh auth login` setup.
459
+ - README clarifies `LEMONSQUEEZY_ALLOWED_STORE_IDS` semantics -- list filters now required when the allowlist is set; tools with no `storeId` field at all remain ungated, so pair with the refund cap and rate limit.
460
+ - README notes that `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` counts include `ls_update_license_key` with `disabled: true`.
461
+ - `SEMVER.md` points at `npm run test:integration` for upstream-drift detection instead of the removed nightly workflow.
462
+ - `CLAUDE.md` and `CONTRIBUTING.md` updated to reference the local script and Biome-on-review instead of CI checks.
463
+
464
+ ## [0.5.0] -- 2026-04-23
465
+
466
+ ### Changed
467
+
468
+ - **Breaking.** `ls_update_webhook` now validates `secret` as `min(6).max(40)`, matching `ls_create_webhook`. Previously accepted any string up to 10,000 chars. Callers passing secrets outside the 6-40 range will now be rejected at the MCP boundary.
469
+ - **Breaking.** Email fields across `ls_list_customers`, `ls_create_customer`, `ls_update_customer`, `ls_list_orders`, and `ls_list_subscriptions` now validate as RFC email (`z.string().email().max(320)`), matching the existing `ls_list_affiliates` filter. Non-email inputs (partial matches, malformed addresses) will now be rejected at the MCP boundary.
470
+ - `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` guardrail now also applies to `ls_refund_subscription_invoice`, closing a gap where the cap only gated `ls_refund_order`.
471
+ - `ls_update_subscription` `trialEndsAt` is now `.nullable()` at the schema level. The description always promised `null` ends the trial immediately; the type now supports it.
472
+
473
+ ### Fixed
474
+
475
+ - Integration test env-var restoration now runs in a top-level `after()` hook instead of at module scope, so `LEMONSQUEEZY_API_KEY` stays set to the test key for the duration of the test run. Previously a developer with both `LEMONSQUEEZY_API_KEY` (prod) and `LEMONSQUEEZY_TEST_API_KEY` set could have their prod key used against the test store.
476
+
477
+ ### Docs
478
+
479
+ - README tool count corrected from 59 to 61 (affiliate tools from 0.3.0 plus `ls_refund_subscription_invoice` weren't reflected in the total).
480
+
481
+ ## [0.4.1] -- 2026-04-20
482
+
483
+ ### Security
484
+
485
+ - Override transitive `hono` to `^4.12.14` to clear Dependabot advisory on `hono/jsx` SSR. Not exploitable in this package (MCP does not use `hono/jsx`), but closes the supply-chain scan signal.
486
+
487
+ ### Docs
488
+
489
+ - README links to `@yawlabs/lemonsqueezy-webhook-sink` from the webhook-reconciliation callout.
490
+
491
+ ## [0.4.0] -- 2026-04-20
492
+
493
+ Hardening pass for unattended automation against live billing flows.
494
+
495
+ ### Added
496
+
497
+ - **Guardrails.** Opt-in controls evaluated in a single dispatcher pre-check:
498
+ - `LEMONSQUEEZY_ALLOWED_STORE_IDS` -- allowlist enforced on every tool call that names a store.
499
+ - `LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS` -- per-call cap on `ls_refund_order` to prevent runaway agents from issuing large refunds.
500
+ - `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` -- rolling 60-second circuit breaker on destructive tool calls.
501
+ All three default to unset → disabled, so existing integrations are unaffected until explicitly opted in.
502
+ - **Retry layer** (`src/retry.ts`). Exponential backoff with jitter, capped at 4 attempts and 30s. Retries `429` always (honors `Retry-After`), `5xx` and transport errors only on idempotent methods (`GET`/`DELETE`). Non-idempotent writes fail fast.
503
+ - **Secret loader** (`src/secret.ts`). `LEMONSQUEEZY_API_KEY_COMMAND` invokes an external command (vault CLI, 1Password, etc.) and caches the result for 1 hour. Lets credentials rotate without restarting the server.
504
+ - **Structured logger** (`src/logger.ts`). Opt-in via `LEMONSQUEEZY_LOG=json`. Emits JSON lines to stderr (stdout stays reserved for MCP protocol). Destructive tool calls are tagged `audit: true` with redacted inputs.
505
+ - **Request ID surfacing.** API error messages include upstream `X-Request-Id` when present, so support tickets can be traced.
506
+ - **Read-only integration tests.** `npm run test:integration` hits a live LemonSqueezy store if `LEMONSQUEEZY_TEST_API_KEY` + `LEMONSQUEEZY_TEST_STORE_ID` are set; skips gracefully otherwise. Exercises `ls_get_user`, `ls_get_store`, `ls_list_products`, `ls_list_variants`, `ls_list_orders`, `ls_list_subscriptions`, and 404 error paths. Runs nightly via `.github/workflows/integration.yml`.
507
+ - **`SEMVER.md`** -- documents what counts as a breaking change for this package (tool names, required inputs, return shapes), and what explicitly does not (upstream API drift, internal module names).
508
+
509
+ ### Changed
510
+
511
+ - Input validation on every tool: `z.string()` fields capped at 10,000 chars, `.email()` fields capped at 320 chars. Protects against accidental DoS via oversized payloads.
512
+
513
+ ### Fixed
514
+
515
+ - `parseRetryAfterMs` now correctly falls back to the default 1s when given a negative number like `"-3"` (previously `Date.parse("-3")` returned a finite value and produced a non-sensical delay).
516
+ - Retry policy no longer retries `5xx` on `POST`/`PATCH`/`PUT` -- prevents duplicate writes if a timeout is actually a slow success.
517
+
518
+ ## [0.3.0] -- 2026-04-18
519
+
520
+ ### Added
521
+
522
+ - Affiliate tools (`ls_list_affiliates`, `ls_get_affiliate`).
523
+ - `429` retry with exponential backoff in the API client.
524
+ - `SECURITY.md` -- vulnerability disclosure policy.
525
+ - `CONTRIBUTING.md` -- contributor and AI-agent guidelines.
526
+
527
+ ## [0.2.1] -- 2026-04-16
528
+
529
+ ### Changed
530
+
531
+ - Deduplicated tool handler boilerplate across tool files.
532
+
533
+ ### Fixed
534
+
535
+ - API error handling surfaces upstream error bodies correctly.
536
+
537
+ ### Added
538
+
539
+ - Error-path tests for every tool.
540
+
541
+ ## [0.2.0] -- 2026-04-14
542
+
543
+ ### Added
544
+
545
+ - Input validation via Zod `.describe()` on every tool input field.
546
+
547
+ ### Fixed
548
+
549
+ - `ls_generate_order_invoice` and `ls_generate_subscription_invoice` now hit the correct endpoints and handle the async invoice-generation response shape.
550
+
551
+ ## [0.1.1] -- 2026-04-12
552
+
553
+ ### Added
554
+
555
+ - Edge-case handler tests for fuller coverage across all 59 tools.
556
+
557
+ ## [0.1.0] -- 2026-04-11
558
+
559
+ Initial release. 59 tools covering all 17 LemonSqueezy API resources.
560
+
561
+ [Unreleased]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.9...HEAD
562
+ [0.11.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.11.0...v0.11.1
563
+ [0.11.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.13...v0.11.0
564
+ [0.10.13]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.12...v0.10.13
565
+ [0.10.12]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.11...v0.10.12
566
+ [0.10.11]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.10...v0.10.11
567
+ [0.10.10]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.9...v0.10.10
568
+ [0.10.9]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.8...v0.10.9
569
+ [0.10.8]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.7...v0.10.8
570
+ [0.10.7]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.6...v0.10.7
571
+ [0.10.6]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.5...v0.10.6
572
+ [0.10.5]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.4...v0.10.5
573
+ [0.10.4]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.3...v0.10.4
574
+ [0.10.3]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.2...v0.10.3
575
+ [0.10.2]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.1...v0.10.2
576
+ [0.10.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.10.0...v0.10.1
577
+ [0.10.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.9.3...v0.10.0
578
+ [0.9.3]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.9.2...v0.9.3
579
+ [0.9.2]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.9.1...v0.9.2
580
+ [0.9.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.9.0...v0.9.1
581
+ [0.9.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.8.1...v0.9.0
582
+ [0.8.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.7.1...v0.8.1
583
+ [0.7.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.7.0...v0.7.1
584
+ [0.7.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.6.2...v0.7.0
585
+ [0.6.2]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.6.1...v0.6.2
586
+ [0.6.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.6.0...v0.6.1
587
+ [0.6.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.5.0...v0.6.0
588
+ [0.5.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.4.1...v0.5.0
589
+ [0.4.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.4.0...v0.4.1
590
+ [0.4.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.3.0...v0.4.0
591
+ [0.3.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.2.1...v0.3.0
592
+ [0.2.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.2.0...v0.2.1
593
+ [0.2.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.1.1...v0.2.0
594
+ [0.1.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.1.0...v0.1.1
595
+ [0.1.0]: https://github.com/YawLabs/lemonsqueezy-mcp/releases/tag/v0.1.0