@yawlabs/lemonsqueezy-mcp 1.0.0 → 1.0.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
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.0.2] — 2026-10-06
6
+
7
+ ### Security
8
+ - **The bundled MCP SDK moves from 1.30.0 to 1.32.1** (GHSA-6qxp-vccf-f47h, high: the SDK's OAuth client could send credentials to an authorization server chosen by the MCP server; this server does not import that client, but the bundled SDK version changes). esbuild bundles the SDK into `dist/index.js`, so the dependency floor is now `^1.32.1`. Its `fast-uri`, also bundled through ajv, moves from 3.1.7 to 3.1.8 (GHSA-hrr3-gc8f-f4qj, moderate: inconsistent host case normalization via percent-encoded octets). Development-scope only: `proxy-addr` 2.0.8 (GHSA-jqcg-44mw-7w3h, critical) and `ip-address` 10.7.3 (GHSA-j6r3-76f7-8jcv, GHSA-h3mg-xc3c-68pw), which reach the tree through the SDK's Express transport but are not in the bundle. The existing `hono` override is unchanged. `npm audit` reports 0 vulnerabilities.
9
+
10
+ ### Changed
11
+ - **The oam floor moves from 0.15.2 to 0.18.0, so the `lemonsqueezy-mcp` launcher no longer runs the server on an older oam.** This server is verified on one oam release at a time, and the floor keeps the launcher off anything older than that release. 0.18.0 is verified on the published aarch64-pc-windows-msvc binary, checksum matched against the release SHA256SUMS, through the launcher with `OAM_BIN` pointing at it: a full MCP handshake listing all 64 tools (the same 64 as on Node), `ls_sink_stats` and the `lemonsqueezy://audit-log` resource answering byte-for-byte as they do on Node, and the server process confirmed as that binary (`oam run .../dist/index.js` as the launcher's child; `process.versions.oam` reads 0.18.0). The opt-in sandbox was re-measured on it: with `LEMONSQUEEZY_MCP_SANDBOX=1 LEMONSQUEEZY_MCP_RUNTIME=oam` the launcher spawns `oam --permission --allow-net=api.lemonsqueezy.com --allow-env=... run`, the same 64 tools answer as they do on Node, and under the same flags a file read and a child process are refused with `ERR_ACCESS_DENIED` while a non-granted environment variable is absent from `process.env` rather than an error, as the README describes. **If the launcher finds only an oam from 0.15.2 to 0.17.x, it now falls back to Node under the default `LEMONSQUEEZY_MCP_RUNTIME=auto`, and exits with an error under `LEMONSQUEEZY_MCP_RUNTIME=oam`** -- it says so on stderr, naming the version it found and the floor (measured with oam 0.17.0: `is oam 0.17.0, older than 0.18.0; using Node instead.`). On Node every tool answers the same; what is lost is `LEMONSQUEEZY_MCP_SANDBOX=1`, which needs a fresh oam at the floor, so under `auto` the server then runs without `--permission`, and under `LEMONSQUEEZY_MCP_RUNTIME=oam` it does not start. A client that runs `oam run /path/to/lemonsqueezy-mcp/dist/index.js` directly bypasses the launcher and keeps the oam it names. Run `oam self-update`, or set `LEMONSQUEEZY_MCP_RUNTIME=node` to make the choice explicit.
12
+ - `release.sh` waits up to 600 s, not 300, for npm to serve a new version before the MCP Registry step, gives the post-publish `npx` smoke 60 attempts 10 s apart, not 30, and polls npm up to 120 times 5 s apart in its final check instead of reading once after 3 s. On 2026-09-29 the @yawlabs/fetch-mcp 0.8.2 release spent 295 s of its 300 s gate waiting for npm to serve the new version, and `npx` needed 313 s, 24 of its 30 attempts, to install @yawlabs/lemonsqueezy-mcp 1.0.1. Release tooling only; the server itself is unchanged.
13
+ - `release.sh` runs every mcp-publisher call to the MCP Registry, each login and each publish attempt, under coreutils `timeout` (`MCP_PUBLISH_TIMEOUT_S`, default 90 s) where one (or Homebrew's `gtimeout`) is on PATH -- without one the call runs unbounded, and a warning says so -- because mcp-publisher waits for the registry's answer with no limit of its own and a registry that never answered would have hung the release. A publish attempt that gets no answer is retried on the same 30, 60 and 90 s clock as the registry's own 429-504: one the limit stopped, which used to hang the step, and one whose connection failed or dropped (the client's `error sending request` or `error reading response`), which used to fail it at once. A login the limit stopped says the registry did not answer; one that failed any other way says how to read mcp-publisher's output (a 401 is the registry refusing the token exchange; a 429, a 5xx or a connection error is the registry or the network) instead of naming token scopes, which never fail a login: the registry reads the org role at login and refuses only at publish. A publish refused with a 403 says what the io.github.YawLabs namespace takes: a YawLabs org Owner whose token can read org roles. Release tooling only; the server itself is unchanged.
14
+
15
+ ### Fixed
16
+ - `scripts/update-manifests.mjs` escapes every value it writes into a Ruby string in the Homebrew formula (CodeQL js/incomplete-sanitization). The `desc` line used to escape `"` but not `\`, so a description holding `\"` came out as `\\"`, an escaped backslash and then a closing quote, and it left `#{...}` alone, which Ruby runs as code when brew loads the formula. A new `rubyString()` escapes the backslash first, then `"`, a `#` that starts `#{`, `#@` or `#$` (a plain `#` stays as written, so `brew style` sees no redundant escape), CR and LF, and covers `desc`, `homepage`, `version`, `license`, the download URLs and hashes, and the command name; the formula's class name is checked as a Ruby constant instead. The script body runs only when the file is executed directly (compared by real path, so a junction or symlink still runs it), which lets `src/update-manifests.test.ts` import the helpers and round-trip hostile descriptions through a Ruby double-quoted string reader. For the real package.json the generated formula and Scoop manifest are byte-identical to before; the Scoop manifest was already built with `JSON.stringify`. Release tooling only; the server itself is unchanged.
17
+
18
+ ## [1.0.1] — 2026-09-29
19
+
20
+ ### Changed
21
+
22
+ - **`release.sh` reads npm's per-version document and retries the MCP Registry publish.** The "is this version on npm?" checks (the pre-flight token gate, step 5's already-published skip and the step 8 check) now ask `https://registry.npmjs.org/@yawlabs%2Flemonsqueezy-mcp/<version>` with `curl` instead of `npm view`, whose whole-package document Cloudflare's edge can serve stale for up to 5 minutes after a publish. The step 7 propagation wait already curled that URL; it now goes through the same check, so each request is capped at 20 s and only a 200 counts. Step 5 treats npm's E403 "cannot publish over the previously published versions" as already published rather than as a token error, so re-running straight after a failed later step no longer dies there. Step 7 makes up to 4 attempts at `mcp-publisher publish`, 30, 60 and 90 s apart, but only when the registry says it cannot see the version on npm yet, calls the failure transient, or answers HTTP 429, 502, 503 or 504 itself (with a fresh registry login before each retry); a duplicate version counts as done, and any other failure still stops at once.
23
+
24
+ ### Documentation
25
+ - **The README's Add to Yaw MCP button now sits directly under the title, and the follow badge links to @YawLabs.** The one-click install button and its one-line caption moved above the one-paragraph description, so they are the first thing under the name on npm and GitHub, and the X badge at the bottom points at [@YawLabs](https://x.com/YawLabs) instead of @TokenLimitNews. npm shows the README from the published package, which is why it takes a release to carry this there; the package's code is unchanged from 1.0.0.
26
+
5
27
  ## [1.0.0] — 2026-09-18
6
28
 
7
29
  **1.0.0 locks the public surface.** [SEMVER.md](./SEMVER.md) now spells out what that surface is: tool names and input fields, MCP annotations, the authority class names and each tool's class, which calls count as destructive, the documented env vars, the `lemonsqueezy://audit-log` resource, and the fields the server adds to responses. From here on its MAJOR / MINOR / PATCH rules apply strictly. `src/tools/tools.test.ts` pins the `destructiveHint: true` set, the `isDestructive`-predicate set, the `readOnlyHint: false` set and the full tool-to-class map, so a change to any of them fails `npm test` until it is made deliberately. The changes marked **Breaking** below were made now, before the lock, because each would need a major version afterwards. Tracked in #2.
@@ -676,7 +698,9 @@ Hardening pass for unattended automation against live billing flows.
676
698
 
677
699
  Initial release. 59 tools covering all 17 LemonSqueezy API resources.
678
700
 
679
- [Unreleased]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v1.0.0...HEAD
701
+ [Unreleased]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v1.0.2...HEAD
702
+ [1.0.2]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v1.0.1...v1.0.2
703
+ [1.0.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v1.0.0...v1.0.1
680
704
  [1.0.0]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.14.2...v1.0.0
681
705
  [0.14.2]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.14.1...v0.14.2
682
706
  [0.14.1]: https://github.com/YawLabs/lemonsqueezy-mcp/compare/v0.14.0...v0.14.1
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # @yawlabs/lemonsqueezy-mcp
2
2
 
3
- MCP server for the [LemonSqueezy](https://lemonsqueezy.com) API. Manage your store, products, customers, subscriptions, discounts, license keys, and more from any MCP-compatible AI assistant.
4
-
5
3
  [![Add to Yaw MCP](https://yaw.sh/yaw-mcp-button.svg)](https://yaw.sh/mcp/install?name=LemonSqueezy&command=npx&args=-y%2C%40yawlabs%2Flemonsqueezy-mcp&description=LemonSqueezy%20store%20management%20-%20products%2C%20orders%2C%20subscriptions%2C%20license%20keys&source=https%3A%2F%2Fgithub.com%2FYawLabs%2Flemonsqueezy-mcp)
6
4
 
7
5
  One click adds this to your local Yaw MCP config so it's available in every Yaw Terminal session. Or install manually below.
8
6
 
7
+ MCP server for the [LemonSqueezy](https://lemonsqueezy.com) API. Manage your store, products, customers, subscriptions, discounts, license keys, and more from any MCP-compatible AI assistant.
8
+
9
9
  ## Quick start
10
10
 
11
11
  ```bash
@@ -338,23 +338,23 @@ npm run check:containerfile # release.sh step 1 runs this too; non-zero exit me
338
338
 
339
339
  ## Running on oam.js (optional)
340
340
 
341
- [oam.js](https://oamjs.org) runs this server unmodified, and the launcher only runs the server on an oam that is **0.15.2 or newer**. Verified against oam 0.9.0: full MCP handshake, all 64 tools, the `lemonsqueezy://audit-log` resource, working `fetch`, and guardrail rejections with error text identical to Node. On oam 0.15.2 the MCP handshake and all 64 tools have been re-verified through the launcher, with and without the sandbox below.
341
+ [oam.js](https://oamjs.org) runs this server unmodified, and the launcher only runs the server on an oam that is **0.18.0 or newer**. Verified against oam 0.9.0: full MCP handshake, all 64 tools, the `lemonsqueezy://audit-log` resource, working `fetch`, and guardrail rejections with error text identical to Node. On the published oam 0.18.0 the MCP handshake, all 64 tools (the same 64 as on Node), a tool call, and the audit-log resource have been re-verified through the launcher, with and without the sandbox below.
342
342
 
343
- **oam 0.15.2 is the minimum.** The launcher picks the newest oam it can find at or above it, never serves on an older one, and falls back to Node when there is none (`LEMONSQUEEZY_MCP_RUNTIME=oam` turns that into a hard error). A floor matters here: releases before 0.9.0 ran `child_process.execFile` arguments through a shell, which was reachable whenever `LEMONSQUEEZY_API_KEY_COMMAND` is configured -- that feature runs the key command with `execFile`, deliberately without a shell, and on those releases its arguments were re-split by one anyway.
343
+ **oam 0.18.0 is the minimum.** The launcher picks the newest oam it can find at or above it, never serves on an older one, and falls back to Node when there is none (`LEMONSQUEEZY_MCP_RUNTIME=oam` turns that into a hard error). A floor matters here: releases before 0.9.0 ran `child_process.execFile` arguments through a shell, which was reachable whenever `LEMONSQUEEZY_API_KEY_COMMAND` is configured -- that feature runs the key command with `execFile`, deliberately without a shell, and on those releases its arguments were re-split by one anyway.
344
344
 
345
345
  The published `lemonsqueezy-mcp` command (`bin/lemonsqueezy-mcp.mjs`, which is what `npx` runs) chooses its runtime from these variables:
346
346
 
347
347
  | Variable | Default | Purpose |
348
348
  |----------|---------|---------|
349
- | `LEMONSQUEEZY_MCP_RUNTIME` | `auto` | `auto`: serve on the oam the launcher is already running under if that is 0.15.2 or newer (unless `LEMONSQUEEZY_MCP_SANDBOX=1`); otherwise run on the newest oam binary it can find at 0.15.2 or newer (see `OAM_BIN`); otherwise on Node. An oam host older than 0.15.2 never serves the server itself -- it hands off to the newest usable oam, or to Node on `PATH`, or exits with an error when there is neither. On stderr, an `OAM_BIN` that was passed over is always named. The other oam binaries passed over, and any `oam.cmd` / `oam.bat` shim, are named only when no usable oam is found -- an older copy skipped for a newer one is not mentioned. A chosen oam that fails to launch, and a handoff to Node from an oam host older than 0.15.2, are always reported; when such a host hands off to a newer oam instead, nothing is printed. `oam`: the same, but exit with an error instead of falling back. `node`: always Node -- in-process under `npx`, and handed off to Node on `PATH` when a client launches the command with `oam run`. Case-insensitive, and any other value behaves like `auto`. |
350
- | `OAM_BIN` | unset | Path to an oam binary to use in preference to discovery, when it is 0.15.2 or newer. If it does not exist, is older, or will not run, the launcher says so on stderr and carries on with discovery. Discovery looks in the installed location (`%LOCALAPPDATA%\oam\bin` then `~/.oam/bin` on Windows, `~/.oam/bin` elsewhere) and on `PATH`, asks every oam it finds for its version, and uses the newest; on a tie the installed copy wins. On Windows only `oam.exe` counts: an `oam.cmd` / `oam.bat` shim is never run, and is named on stderr only when no usable oam is found. Ignored under `LEMONSQUEEZY_MCP_RUNTIME=node`, and when already running on oam 0.15.2+ without the sandbox. |
349
+ | `LEMONSQUEEZY_MCP_RUNTIME` | `auto` | `auto`: serve on the oam the launcher is already running under if that is 0.18.0 or newer (unless `LEMONSQUEEZY_MCP_SANDBOX=1`); otherwise run on the newest oam binary it can find at 0.18.0 or newer (see `OAM_BIN`); otherwise on Node. An oam host older than 0.18.0 never serves the server itself -- it hands off to the newest usable oam, or to Node on `PATH`, or exits with an error when there is neither. On stderr, an `OAM_BIN` that was passed over is always named. The other oam binaries passed over, and any `oam.cmd` / `oam.bat` shim, are named only when no usable oam is found -- an older copy skipped for a newer one is not mentioned. A chosen oam that fails to launch, and a handoff to Node from an oam host older than 0.18.0, are always reported; when such a host hands off to a newer oam instead, nothing is printed. `oam`: the same, but exit with an error instead of falling back. `node`: always Node -- in-process under `npx`, and handed off to Node on `PATH` when a client launches the command with `oam run`. Case-insensitive, and any other value behaves like `auto`. |
350
+ | `OAM_BIN` | unset | Path to an oam binary to use in preference to discovery, when it is 0.18.0 or newer. If it does not exist, is older, or will not run, the launcher says so on stderr and carries on with discovery. Discovery looks in the installed location (`%LOCALAPPDATA%\oam\bin` then `~/.oam/bin` on Windows, `~/.oam/bin` elsewhere) and on `PATH`, asks every oam it finds for its version, and uses the newest; on a tie the installed copy wins. On Windows only `oam.exe` counts: an `oam.cmd` / `oam.bat` shim is never run, and is named on stderr only when no usable oam is found. Ignored under `LEMONSQUEEZY_MCP_RUNTIME=node`, and when already running on oam 0.18.0+ without the sandbox. |
351
351
  | `LEMONSQUEEZY_MCP_SANDBOX` | unset | `1` runs the server under `--permission`; see below. |
352
352
 
353
353
  ### Sandboxing (opt-in)
354
354
 
355
355
  Set `LEMONSQUEEZY_MCP_SANDBOX=1` to run under oam's `--permission` model: network restricted to `api.lemonsqueezy.com` (plus the host of `LEMONSQUEEZY_SINK_URL` when set), filesystem denied outright, and child-process denied unless `LEMONSQUEEZY_API_KEY_COMMAND` is configured.
356
356
 
357
- `--permission` is a process-level flag, so the launcher always starts a fresh oam for it -- even when a client already launches the command under oam 0.15.2+. That is a preference, not a guarantee: if no usable oam can be started, `LEMONSQUEEZY_MCP_RUNTIME=auto` still serves the server **without** `--permission`, and `LEMONSQUEEZY_MCP_RUNTIME=oam` exits with an error instead.
357
+ `--permission` is a process-level flag, so the launcher always starts a fresh oam for it -- even when a client already launches the command under oam 0.18.0+. That is a preference, not a guarantee: if no usable oam can be started, `LEMONSQUEEZY_MCP_RUNTIME=auto` still serves the server **without** `--permission`, and `LEMONSQUEEZY_MCP_RUNTIME=oam` exits with an error instead.
358
358
 
359
359
  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, so an under-granted `LEMONSQUEEZY_API_KEY` reads as "unauthenticated" rather than "denied". The env allow-list in the launcher is derived from what the shipped bundle actually reads -- if you add a new `process.env` lookup, extend that list with it.
360
360
 
@@ -401,7 +401,7 @@ It then runs eight steps on the workstation:
401
401
  4. Turn `## [Unreleased]` in `CHANGELOG.md` into `## [X.Y.Z]` (when `[Unreleased]` is empty, write that section from the commit subjects since the previous tag instead, with a warning), commit, create the annotated tag `vX.Y.Z`, and push `main` with the tag
402
402
  5. `npm publish --access public`
403
403
  6. Create the GitHub release, with the notes taken from the `## [X.Y.Z]` section of `CHANGELOG.md`
404
- 7. Wait for npm to serve the new version (up to 5 minutes, then carry on with a warning), smoke-test `npx -y @yawlabs/lemonsqueezy-mcp@X.Y.Z --version`, then publish `server.json` to the [Official MCP Registry](https://registry.modelcontextprotocol.io) with `mcp-publisher`
404
+ 7. Wait for npm to serve the new version (up to 10 minutes, then carry on with a warning), smoke-test `npx -y @yawlabs/lemonsqueezy-mcp@X.Y.Z --version` (up to 60 attempts 10 seconds apart), then publish `server.json` to the [Official MCP Registry](https://registry.modelcontextprotocol.io) with `mcp-publisher`
405
405
  8. Verify the npm version, `package.json` and the tag
406
406
 
407
407
  Re-running after a partial failure is safe. `./release.sh X.Y.Z` detects the version bump, commit, tag, npm publish and GitHub release that already landed and skips them; lint, test, the push and the npx smoke test run again. The MCP Registry publish in step 7 is not skipped, so a re-run after it has succeeded fails there on the duplicate version.
@@ -418,7 +418,7 @@ One-time setup on each machine:
418
418
  ```
419
419
 
420
420
  **Never run `npm login --auth-type=web`.** It overwrites the automation token with a 2FA-bound web session, and the next publish fails with `EOTP`.
421
- - **`gh auth login`.** The pre-flight requires it, and step 6 creates the release with it. Step 7 logs `mcp-publisher` in with its token (`gh auth token`) unless `MCP_REGISTRY_TOKEN` is set; either way the token needs `read:org` access to YawLabs, which the registry checks for the `io.github.YawLabs/*` namespace. `mcp-publisher` is downloaded to `~/.local/bin` on first use; set `MCP_PUBLISHER` to use another copy.
421
+ - **`gh auth login`.** The pre-flight requires it, and step 6 creates the release with it. Step 7 logs `mcp-publisher` in with its token (`gh auth token`) unless `MCP_REGISTRY_TOKEN` is set; either way the registry grants the `io.github.YawLabs/*` namespace only to a YawLabs org Owner whose token can read org roles (`read:org` does; so do `repo`, `user`, `write:org` and `admin:org`). It reads the role at login but refuses only at publish, so a token that cannot read it still logs in and the publish gets a 403. `mcp-publisher` is downloaded to `~/.local/bin` on first use; set `MCP_PUBLISHER` to use another copy.
422
422
  - **`jq`, `curl` and `tar` on `PATH`.** Step 3 edits `server.json` with `jq`. Step 7 downloads `mcp-publisher` with `curl` and `tar` on first use, unless `MCP_PUBLISHER` points at an existing copy. It also polls npm with `curl`, and skips that wait with a warning if `curl` is missing.
423
423
  - **Rights to push to `main` and create `v*` tags.** The repository rulesets require a pull request for `main` and restrict `v*` tag creation, only organization admins bypass them, and step 4 pushes both directly.
424
424
 
@@ -426,4 +426,4 @@ One-time setup on each machine:
426
426
 
427
427
  MIT
428
428
 
429
- [![Follow @TokenLimitNews on X](https://img.shields.io/badge/follow-%40TokenLimitNews-000000?logo=x&logoColor=white)](https://x.com/TokenLimitNews)
429
+ [![Follow @YawLabs on X](https://img.shields.io/badge/follow-%40YawLabs-000000?logo=x&logoColor=white)](https://x.com/YawLabs)
@@ -96,7 +96,7 @@
96
96
  * shipped bundle actually reads -- keep it in step with the bundle.
97
97
  *
98
98
  * MINIMUM OAM VERSION
99
- * OAM_MIN, 0.15.2, is a floor: the oam this server was last verified on, not
99
+ * OAM_MIN, 0.18.0, is a floor: the oam this server was last verified on, not
100
100
  * a claim that it is the newest oam. Raise it once a newer oam has been
101
101
  * re-verified here (handshake and all 64 tools through this launcher). The
102
102
  * server only runs on an oam at or above the floor; an older one is never
@@ -128,7 +128,7 @@ import { delimiter, join } from "node:path";
128
128
  import { fileURLToPath } from "node:url";
129
129
 
130
130
  /** Oldest oam this server is served on. See MINIMUM OAM VERSION above. */
131
- const OAM_MIN = [0, 15, 2];
131
+ const OAM_MIN = [0, 18, 0];
132
132
 
133
133
  /**
134
134
  * Bound on each `oam --version` probe. A healthy oam answers in milliseconds;
package/dist/index.js CHANGED
@@ -4148,13 +4148,14 @@ var require_fast_uri = __commonJS({
4148
4148
  if (!malformedIPLiteral) {
4149
4149
  malformedHost = canonicalizeHost(parsed, options, schemeHandler, isIP);
4150
4150
  }
4151
- if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
4152
- if (uri.indexOf("%") !== -1) {
4153
- if (parsed.host !== void 0 && !malformedIPLiteral) {
4154
- const host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
4155
- parsed.host = reescapeHostDelimiters(host, isIP);
4156
- }
4151
+ if (uri.indexOf("%") !== -1 && parsed.host !== void 0 && !malformedIPLiteral) {
4152
+ let host = isIP ? parsed.host : normalizePercentEncoding(parsed.host, true);
4153
+ if (!isIP) {
4154
+ host = normalizePercentEncoding(host.toLowerCase());
4157
4155
  }
4156
+ parsed.host = reescapeHostDelimiters(host, isIP);
4157
+ }
4158
+ if (!schemeHandler || schemeHandler && !schemeHandler.skipNormalize) {
4158
4159
  if (parsed.path) {
4159
4160
  parsed.path = normalizePathEncoding(parsed.path);
4160
4161
  }
@@ -28700,6 +28701,9 @@ var Protocol = class {
28700
28701
  this.setRequestHandler(GetTaskPayloadRequestSchema, async (request, extra) => {
28701
28702
  const handleTaskResult = async () => {
28702
28703
  const taskId = request.params.taskId;
28704
+ if (!await this._taskStore.getTask(taskId, extra.sessionId)) {
28705
+ throw new McpError(ErrorCode.InvalidParams, `Task not found: ${taskId}`);
28706
+ }
28703
28707
  if (this._taskMessageQueue) {
28704
28708
  let queuedMessage;
28705
28709
  while (queuedMessage = await this._taskMessageQueue.dequeue(taskId, extra.sessionId)) {
@@ -28730,12 +28734,12 @@ var Protocol = class {
28730
28734
  throw new McpError(ErrorCode.InvalidParams, `Task not found: ${taskId}`);
28731
28735
  }
28732
28736
  if (!isTerminal(task.status)) {
28733
- await this._waitForTaskUpdate(taskId, extra.signal);
28737
+ await this._waitForTaskUpdate(taskId, extra.signal, extra.sessionId);
28734
28738
  return await handleTaskResult();
28735
28739
  }
28736
28740
  if (isTerminal(task.status)) {
28737
28741
  const result = await this._taskStore.getTaskResult(taskId, extra.sessionId);
28738
- this._clearTaskQueue(taskId);
28742
+ this._clearTaskQueue(taskId, extra.sessionId);
28739
28743
  return {
28740
28744
  ...result,
28741
28745
  _meta: {
@@ -28772,7 +28776,7 @@ var Protocol = class {
28772
28776
  throw new McpError(ErrorCode.InvalidParams, `Cannot cancel task in terminal status: ${task.status}`);
28773
28777
  }
28774
28778
  await this._taskStore.updateTaskStatus(request.params.taskId, "cancelled", "Client cancelled task execution.", extra.sessionId);
28775
- this._clearTaskQueue(request.params.taskId);
28779
+ this._clearTaskQueue(request.params.taskId, extra.sessionId);
28776
28780
  const cancelledTask = await this._taskStore.getTask(request.params.taskId, extra.sessionId);
28777
28781
  if (!cancelledTask) {
28778
28782
  throw new McpError(ErrorCode.InvalidParams, `Task not found after cancellation: ${request.params.taskId}`);
@@ -28900,6 +28904,19 @@ var Protocol = class {
28900
28904
  const handler = this._requestHandlers.get(request.method) ?? this.fallbackRequestHandler;
28901
28905
  const capturedTransport = this._transport;
28902
28906
  const relatedTaskId = request.params?._meta?.[RELATED_TASK_META_KEY]?.taskId;
28907
+ const sessionId = capturedTransport?.sessionId;
28908
+ const store = this._taskStore;
28909
+ let relatedTaskFound = true;
28910
+ let relatedTaskLookup;
28911
+ if (relatedTaskId && store && this._taskMessageQueue && sessionId !== void 0) {
28912
+ relatedTaskFound = false;
28913
+ relatedTaskLookup = (async () => {
28914
+ if (!await store.getTask(relatedTaskId, sessionId)) {
28915
+ throw new McpError(ErrorCode.InvalidParams, `Task not found: ${relatedTaskId}`);
28916
+ }
28917
+ relatedTaskFound = true;
28918
+ })();
28919
+ }
28903
28920
  if (handler === void 0) {
28904
28921
  const errorResponse = {
28905
28922
  jsonrpc: "2.0",
@@ -28909,7 +28926,10 @@ var Protocol = class {
28909
28926
  message: "Method not found"
28910
28927
  }
28911
28928
  };
28912
- if (relatedTaskId && this._taskMessageQueue) {
28929
+ if (relatedTaskId && relatedTaskLookup) {
28930
+ const queuedError = { type: "error", message: errorResponse, timestamp: Date.now() };
28931
+ relatedTaskLookup.then(() => this._enqueueTaskMessage(relatedTaskId, queuedError, sessionId), () => capturedTransport?.send(errorResponse)).catch((error51) => this._onerror(new Error(`Failed to send an error response: ${error51}`)));
28932
+ } else if (relatedTaskId && this._taskMessageQueue) {
28913
28933
  this._enqueueTaskMessage(relatedTaskId, {
28914
28934
  type: "error",
28915
28935
  message: errorResponse,
@@ -28960,7 +28980,10 @@ var Protocol = class {
28960
28980
  closeSSEStream: extra?.closeSSEStream,
28961
28981
  closeStandaloneSSEStream: extra?.closeStandaloneSSEStream
28962
28982
  };
28963
- Promise.resolve().then(() => {
28983
+ (relatedTaskLookup ?? Promise.resolve()).then(() => {
28984
+ if (relatedTaskLookup && abortController.signal.aborted) {
28985
+ throw new McpError(ErrorCode.ConnectionClosed, "Request was cancelled");
28986
+ }
28964
28987
  if (taskCreationParams) {
28965
28988
  this.assertTaskHandlerCapability(request.method);
28966
28989
  }
@@ -28995,7 +29018,7 @@ var Protocol = class {
28995
29018
  ...error51["data"] !== void 0 && { data: error51["data"] }
28996
29019
  }
28997
29020
  };
28998
- if (relatedTaskId && this._taskMessageQueue) {
29021
+ if (relatedTaskId && this._taskMessageQueue && relatedTaskFound) {
28999
29022
  await this._enqueueTaskMessage(relatedTaskId, {
29000
29023
  type: "error",
29001
29024
  message: errorResponse,
@@ -29475,7 +29498,7 @@ var Protocol = class {
29475
29498
  throw new Error("Cannot enqueue task message: taskStore and taskMessageQueue are not configured");
29476
29499
  }
29477
29500
  const maxQueueSize = this._options?.maxTaskQueueSize;
29478
- await this._taskMessageQueue.enqueue(taskId, message, sessionId, maxQueueSize);
29501
+ await this._taskMessageQueue.enqueue(taskId, message, sessionId ?? this._transport?.sessionId, maxQueueSize);
29479
29502
  }
29480
29503
  /**
29481
29504
  * Clears the message queue for a task and rejects any pending request resolvers.
@@ -29504,12 +29527,13 @@ var Protocol = class {
29504
29527
  * Uses polling to check for updates at the task's configured poll interval.
29505
29528
  * @param taskId The task ID to wait for
29506
29529
  * @param signal Abort signal to cancel the wait
29530
+ * @param sessionId Session of the request that waits, passed to the task store
29507
29531
  * @returns Promise that resolves when an update occurs or rejects if aborted
29508
29532
  */
29509
- async _waitForTaskUpdate(taskId, signal) {
29533
+ async _waitForTaskUpdate(taskId, signal, sessionId) {
29510
29534
  let interval = this._options?.defaultTaskPollInterval ?? 1e3;
29511
29535
  try {
29512
- const task = await this._taskStore?.getTask(taskId);
29536
+ const task = await this._taskStore?.getTask(taskId, sessionId);
29513
29537
  if (task?.pollInterval) {
29514
29538
  interval = task.pollInterval;
29515
29539
  }
@@ -30388,6 +30412,42 @@ var ExperimentalMcpServerTasks = class {
30388
30412
  };
30389
30413
 
30390
30414
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js
30415
+ function toolInputElementCount(value, max) {
30416
+ let count = 0;
30417
+ const stack = [value];
30418
+ while (stack.length > 0) {
30419
+ const node = stack.pop();
30420
+ if (node === null || typeof node !== "object")
30421
+ continue;
30422
+ if (Array.isArray(node)) {
30423
+ for (const child of node) {
30424
+ if (++count > max)
30425
+ return count;
30426
+ if (child !== null && typeof child === "object")
30427
+ stack.push(child);
30428
+ }
30429
+ } else {
30430
+ for (const key in node) {
30431
+ if (!Object.prototype.hasOwnProperty.call(node, key))
30432
+ continue;
30433
+ if (++count > max)
30434
+ return count;
30435
+ const child = node[key];
30436
+ if (child !== null && typeof child === "object")
30437
+ stack.push(child);
30438
+ }
30439
+ }
30440
+ }
30441
+ return count;
30442
+ }
30443
+ function resolveMaxToolInputElements(value) {
30444
+ if (value === void 0 || value === Infinity)
30445
+ return void 0;
30446
+ if (typeof value !== "number" || Number.isNaN(value) || value < 1) {
30447
+ throw new RangeError(`maxToolInputElements must be a number of at least 1, or Infinity, got ${String(value)}`);
30448
+ }
30449
+ return value;
30450
+ }
30391
30451
  var McpServer = class {
30392
30452
  constructor(serverInfo, options) {
30393
30453
  this._registeredResources = {};
@@ -30399,6 +30459,7 @@ var McpServer = class {
30399
30459
  this._resourceHandlersInitialized = false;
30400
30460
  this._promptHandlersInitialized = false;
30401
30461
  this.server = new Server(serverInfo, options);
30462
+ this._maxToolInputElements = resolveMaxToolInputElements(options?.maxToolInputElements);
30402
30463
  }
30403
30464
  /**
30404
30465
  * Access experimental features.
@@ -30529,12 +30590,15 @@ var McpServer = class {
30529
30590
  * Validates tool input arguments against the tool's input schema.
30530
30591
  */
30531
30592
  async validateToolInput(tool, args, toolName) {
30593
+ if (this._maxToolInputElements !== void 0 && toolInputElementCount(args, this._maxToolInputElements) > this._maxToolInputElements) {
30594
+ throw new McpError(ErrorCode.InvalidParams, `Invalid arguments for tool ${toolName}: arguments contain more than the maximum of ${this._maxToolInputElements} elements`);
30595
+ }
30532
30596
  if (!tool.inputSchema) {
30533
30597
  return void 0;
30534
30598
  }
30535
30599
  const inputObj = normalizeObjectSchema(tool.inputSchema);
30536
30600
  const schemaToParse = inputObj ?? tool.inputSchema;
30537
- const parseResult = await safeParseAsync2(schemaToParse, args);
30601
+ const parseResult = await safeParseAsync2(schemaToParse, args ?? {});
30538
30602
  if (!parseResult.success) {
30539
30603
  const error51 = "error" in parseResult ? parseResult.error : "Unknown error";
30540
30604
  const errorMessage = getParseErrorMessage(error51);
@@ -30772,7 +30836,7 @@ var McpServer = class {
30772
30836
  }
30773
30837
  if (prompt.argsSchema) {
30774
30838
  const argsObj = normalizeObjectSchema(prompt.argsSchema);
30775
- const parseResult = await safeParseAsync2(argsObj, request.params.arguments);
30839
+ const parseResult = await safeParseAsync2(argsObj, request.params.arguments ?? {});
30776
30840
  if (!parseResult.success) {
30777
30841
  const error51 = "error" in parseResult ? parseResult.error : "Unknown error";
30778
30842
  const errorMessage = getParseErrorMessage(error51);
@@ -34160,7 +34224,7 @@ function readAuditLogResource(uri) {
34160
34224
  }
34161
34225
 
34162
34226
  // src/index.ts
34163
- var version2 = true ? "1.0.0" : createRequire(import.meta.url)("../package.json").version;
34227
+ var version2 = true ? "1.0.2" : createRequire(import.meta.url)("../package.json").version;
34164
34228
  var subcommand = process.argv[2];
34165
34229
  if (subcommand === "version" || subcommand === "--version") {
34166
34230
  console.log(version2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/lemonsqueezy-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "mcpName": "io.github.YawLabs/lemonsqueezy-mcp",
5
5
  "description": "Lemon Squeezy MCP server: stores, orders, subscriptions, license keys, refunds, and webhooks.",
6
6
  "license": "MIT",
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "devDependencies": {
65
65
  "@biomejs/biome": "^2.4.12",
66
- "@modelcontextprotocol/sdk": "^1.30.0",
66
+ "@modelcontextprotocol/sdk": "^1.32.1",
67
67
  "@types/node": "^26.1.1",
68
68
  "esbuild": "^0.28.1",
69
69
  "postject": "^1.0.0-alpha.6",