@yawlabs/lemonsqueezy-mcp 0.9.3 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +11 -2
  2. package/dist/index.js +346 -182
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -100,7 +100,7 @@ Add to `claude_desktop_config.json`:
100
100
  }
101
101
  ```
102
102
 
103
- ## Tools (61)
103
+ ## Tools (64)
104
104
 
105
105
  ### Users
106
106
  - `ls_get_user` — Get the authenticated user
@@ -201,9 +201,16 @@ Add to `claude_desktop_config.json`:
201
201
  - `ls_validate_license` — Validate a license key (no API key required)
202
202
  - `ls_deactivate_license` — Deactivate a license key instance (no API key required)
203
203
 
204
+ ### Webhook sink (optional)
205
+ Bridge to a separate [@yawlabs/lemonsqueezy-webhook-sink](https://github.com/YawLabs/lemonsqueezy-webhook-sink) process so the agent can reconcile against webhooks that actually fired. Tools are always registered; if `LEMONSQUEEZY_SINK_URL` / `LEMONSQUEEZY_SINK_ADMIN_TOKEN` are unset, calls return a clear "not configured" error.
206
+
207
+ - `ls_sink_events_list` — List webhook events the sink has received (filter by `since` / `type` / `limit`)
208
+ - `ls_sink_event_mark_processed` — Mark a sink event as processed by your consumer (idempotent)
209
+ - `ls_sink_stats` — Get total events, unprocessed count, and last-received timestamp
210
+
204
211
  ## Features
205
212
 
206
- - **Full API coverage** — All 17 LemonSqueezy API resources with 61 tools
213
+ - **Full API coverage** — All 17 LemonSqueezy API resources with 61 tools, plus 3 bridge tools to an optional [@yawlabs/lemonsqueezy-webhook-sink](https://github.com/YawLabs/lemonsqueezy-webhook-sink) for webhook reconciliation
207
214
  - **JSON:API support** — Filtering, pagination, and relationship inclusion on all list/get operations
208
215
  - **Zero runtime dependencies** — Single bundled file for instant `npx` startup
209
216
  - **License API** — Activate, validate, and deactivate license keys without an API key
@@ -228,6 +235,8 @@ All configuration is via environment variables. Only `LEMONSQUEEZY_API_KEY` (or
228
235
  | `LEMONSQUEEZY_DISABLE_CLASSES` | Comma-separated list of [authority classes](#authority-classes) to refuse outright. Any tool whose class is listed returns a `guardrail_block` before the API call is attempted. Example: `LEMONSQUEEZY_DISABLE_CLASSES=money,recurring,pii` lets an agent run reads but blocks refunds, subscription changes, and customer-record access. Unknown class names throw at server startup. |
229
236
  | `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS` | Per-class rolling rate limits, comma-separated. Each entry is `class:N`, `class:N/m`, or `class:N/h` (bare numbers default to per-minute). Example: `money:2/h,recurring:5/h,key:10/m`. Composes with `LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT` — both must pass. In-process per server instance. |
230
237
  | `LEMONSQUEEZY_LOG` | Structured-log verbosity to stderr. Set to `all` (or legacy `json`) to log every tool and HTTP call, `audit` to log only destructive-call audit entries plus errors (recommended for production), `error` to log only failures. Unset: no logs. Destructive calls are tagged `audit: true` and include their inputs. |
238
+ | `LEMONSQUEEZY_SINK_URL` | Base URL of an optional [@yawlabs/lemonsqueezy-webhook-sink](https://github.com/YawLabs/lemonsqueezy-webhook-sink) instance (e.g. `https://webhooks.example.com`). Trailing slashes are stripped. Enables the `ls_sink_*` reconciliation tools below. Unset: the tools are still registered but return a "not configured" error when called. |
239
+ | `LEMONSQUEEZY_SINK_ADMIN_TOKEN` | Bearer token for the sink's admin endpoints. Must match the sink's `WEBHOOK_SINK_ADMIN_TOKEN`. Required when `LEMONSQUEEZY_SINK_URL` is set; if the sink itself was started without an admin token, its admin endpoints return 404 and `ls_sink_*` calls surface that diagnostically. |
231
240
 
232
241
  ### Logging format
233
242
 
package/dist/index.js CHANGED
@@ -30200,6 +30200,189 @@ var StdioServerTransport = class {
30200
30200
  }
30201
30201
  };
30202
30202
 
30203
+ // src/guardrails.ts
30204
+ var GuardrailError = class extends Error {
30205
+ constructor(message) {
30206
+ super(message);
30207
+ this.name = "GuardrailError";
30208
+ }
30209
+ };
30210
+ var AUTHORITY_CLASSES = ["read", "pii", "mutate", "money", "recurring", "key", "webhook"];
30211
+ function isAuthorityClass(s) {
30212
+ return AUTHORITY_CLASSES.includes(s);
30213
+ }
30214
+ var cachedOptions = null;
30215
+ var destructiveTimestamps = [];
30216
+ var classTimestamps = /* @__PURE__ */ new Map();
30217
+ function readNumber(name, raw) {
30218
+ if (raw === void 0 || raw.trim() === "") return null;
30219
+ const n = Number(raw);
30220
+ if (!Number.isFinite(n) || n < 0) {
30221
+ throw new Error(`${name} must be a non-negative number (got ${JSON.stringify(raw)})`);
30222
+ }
30223
+ return n;
30224
+ }
30225
+ function parseDisabledClasses(raw) {
30226
+ if (!raw || raw.trim() === "") return null;
30227
+ const out = /* @__PURE__ */ new Set();
30228
+ for (const part of raw.split(",")) {
30229
+ const cls = part.trim();
30230
+ if (!cls) continue;
30231
+ if (!isAuthorityClass(cls)) {
30232
+ throw new Error(
30233
+ `LEMONSQUEEZY_DISABLE_CLASSES contains unknown class ${JSON.stringify(cls)} (expected one of: ${AUTHORITY_CLASSES.join(", ")})`
30234
+ );
30235
+ }
30236
+ out.add(cls);
30237
+ }
30238
+ return out.size > 0 ? out : null;
30239
+ }
30240
+ function parseClassRateLimits(raw) {
30241
+ if (!raw || raw.trim() === "") return null;
30242
+ const out = /* @__PURE__ */ new Map();
30243
+ for (const part of raw.split(",")) {
30244
+ const segment = part.trim();
30245
+ if (!segment) continue;
30246
+ const colon = segment.indexOf(":");
30247
+ if (colon < 0) {
30248
+ throw new Error(
30249
+ `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry missing colon: ${JSON.stringify(segment)} (expected class:N or class:N/h)`
30250
+ );
30251
+ }
30252
+ const cls = segment.slice(0, colon).trim();
30253
+ if (!isAuthorityClass(cls)) {
30254
+ throw new Error(
30255
+ `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS contains unknown class ${JSON.stringify(cls)} (expected one of: ${AUTHORITY_CLASSES.join(", ")})`
30256
+ );
30257
+ }
30258
+ const specRaw = segment.slice(colon + 1).trim();
30259
+ const slash = specRaw.indexOf("/");
30260
+ const numPart = slash >= 0 ? specRaw.slice(0, slash).trim() : specRaw;
30261
+ const unitPart = slash >= 0 ? specRaw.slice(slash + 1).trim().toLowerCase() : "m";
30262
+ const n = Number(numPart);
30263
+ if (!Number.isFinite(n) || n < 0 || numPart === "") {
30264
+ throw new Error(`LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry has invalid number: ${JSON.stringify(segment)}`);
30265
+ }
30266
+ let windowMs;
30267
+ if (unitPart === "m") windowMs = 6e4;
30268
+ else if (unitPart === "h") windowMs = 36e5;
30269
+ else {
30270
+ throw new Error(
30271
+ `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry has invalid unit (expected m or h): ${JSON.stringify(segment)}`
30272
+ );
30273
+ }
30274
+ out.set(cls, { limit: n, windowMs });
30275
+ }
30276
+ return out.size > 0 ? out : null;
30277
+ }
30278
+ function loadOptions() {
30279
+ if (cachedOptions) return cachedOptions;
30280
+ const allowed = process.env.LEMONSQUEEZY_ALLOWED_STORE_IDS;
30281
+ const allowedSet = allowed ? new Set(
30282
+ allowed.split(",").map((s) => s.trim()).filter(Boolean)
30283
+ ) : null;
30284
+ cachedOptions = {
30285
+ allowedStoreIds: allowedSet && allowedSet.size > 0 ? allowedSet : null,
30286
+ maxRefundAmountCents: readNumber(
30287
+ "LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS",
30288
+ process.env.LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS
30289
+ ),
30290
+ rateLimitPerMinute: readNumber(
30291
+ "LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT",
30292
+ process.env.LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT
30293
+ ),
30294
+ disabledClasses: parseDisabledClasses(process.env.LEMONSQUEEZY_DISABLE_CLASSES),
30295
+ classRateLimits: parseClassRateLimits(process.env.LEMONSQUEEZY_RATE_LIMIT_PER_CLASS)
30296
+ };
30297
+ return cachedOptions;
30298
+ }
30299
+ function checkStoreAllowed(storeId) {
30300
+ if (!storeId) return;
30301
+ const o = loadOptions();
30302
+ if (!o.allowedStoreIds) return;
30303
+ if (!o.allowedStoreIds.has(String(storeId))) {
30304
+ throw new GuardrailError(`Store ID ${storeId} is not in LEMONSQUEEZY_ALLOWED_STORE_IDS allowlist`);
30305
+ }
30306
+ }
30307
+ function checkRefundAmount(cents) {
30308
+ const o = loadOptions();
30309
+ if (o.maxRefundAmountCents === null) return;
30310
+ if (cents > o.maxRefundAmountCents) {
30311
+ throw new GuardrailError(
30312
+ `Refund amount ${cents} cents exceeds LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS (${o.maxRefundAmountCents})`
30313
+ );
30314
+ }
30315
+ }
30316
+ function checkDestructiveRateLimit(now = Date.now()) {
30317
+ const o = loadOptions();
30318
+ if (o.rateLimitPerMinute === null) return;
30319
+ const cutoff = now - 6e4;
30320
+ destructiveTimestamps = destructiveTimestamps.filter((t) => t > cutoff);
30321
+ if (destructiveTimestamps.length >= o.rateLimitPerMinute) {
30322
+ throw new GuardrailError(`Destructive call rate limit exceeded (${o.rateLimitPerMinute}/min). Wait and retry.`);
30323
+ }
30324
+ destructiveTimestamps.push(now);
30325
+ }
30326
+ function isStoreAllowlistActive() {
30327
+ return loadOptions().allowedStoreIds !== null;
30328
+ }
30329
+ function loadGuardrailOptions() {
30330
+ loadOptions();
30331
+ }
30332
+ function isPresent(value) {
30333
+ if (value === void 0 || value === null) return false;
30334
+ if (typeof value === "string" && value === "") return false;
30335
+ if (Array.isArray(value) && value.length === 0) return false;
30336
+ return true;
30337
+ }
30338
+ function checkStoreScopedToolInput(tool, input) {
30339
+ const toolAcceptsStoreId = "storeId" in tool.inputSchema.shape;
30340
+ if (toolAcceptsStoreId) {
30341
+ const raw = input.storeId;
30342
+ if (raw !== void 0 && raw !== null && raw !== "") {
30343
+ checkStoreAllowed(String(raw));
30344
+ } else if (isStoreAllowlistActive()) {
30345
+ throw new GuardrailError("storeId is required when LEMONSQUEEZY_ALLOWED_STORE_IDS is set");
30346
+ }
30347
+ }
30348
+ if (tool.requiredFilters && tool.requiredFilters.length > 0 && isStoreAllowlistActive()) {
30349
+ const anyPresent = tool.requiredFilters.some((key) => isPresent(input[key]));
30350
+ if (!anyPresent) {
30351
+ throw new GuardrailError(
30352
+ `At least one of [${tool.requiredFilters.join(", ")}] is required when LEMONSQUEEZY_ALLOWED_STORE_IDS is set`
30353
+ );
30354
+ }
30355
+ }
30356
+ }
30357
+ function isDestructiveCall(tool, input) {
30358
+ if (typeof tool.isDestructive === "function") return tool.isDestructive(input);
30359
+ return tool.annotations?.destructiveHint === true;
30360
+ }
30361
+ function checkClassAllowed(cls) {
30362
+ const o = loadOptions();
30363
+ if (!o.disabledClasses) return;
30364
+ if (o.disabledClasses.has(cls)) {
30365
+ throw new GuardrailError(`Tool authority class ${JSON.stringify(cls)} is disabled by LEMONSQUEEZY_DISABLE_CLASSES`);
30366
+ }
30367
+ }
30368
+ function checkClassRateLimit(cls, now = Date.now()) {
30369
+ const o = loadOptions();
30370
+ if (!o.classRateLimits) return;
30371
+ const spec = o.classRateLimits.get(cls);
30372
+ if (!spec) return;
30373
+ const cutoff = now - spec.windowMs;
30374
+ const list = (classTimestamps.get(cls) ?? []).filter((t) => t > cutoff);
30375
+ if (list.length >= spec.limit) {
30376
+ const unit = spec.windowMs === 6e4 ? "min" : spec.windowMs === 36e5 ? "hour" : `${spec.windowMs}ms`;
30377
+ classTimestamps.set(cls, list);
30378
+ throw new GuardrailError(
30379
+ `Class ${JSON.stringify(cls)} rate limit exceeded (${spec.limit}/${unit}). Wait and retry.`
30380
+ );
30381
+ }
30382
+ list.push(now);
30383
+ classTimestamps.set(cls, list);
30384
+ }
30385
+
30203
30386
  // src/logger.ts
30204
30387
  function getLogLevel() {
30205
30388
  const v = process.env.LEMONSQUEEZY_LOG;
@@ -31461,186 +31644,6 @@ var orderItemTools = [
31461
31644
  }
31462
31645
  ];
31463
31646
 
31464
- // src/guardrails.ts
31465
- var GuardrailError = class extends Error {
31466
- constructor(message) {
31467
- super(message);
31468
- this.name = "GuardrailError";
31469
- }
31470
- };
31471
- var AUTHORITY_CLASSES = ["read", "pii", "mutate", "money", "recurring", "key", "webhook"];
31472
- function isAuthorityClass(s) {
31473
- return AUTHORITY_CLASSES.includes(s);
31474
- }
31475
- var cachedOptions = null;
31476
- var destructiveTimestamps = [];
31477
- var classTimestamps = /* @__PURE__ */ new Map();
31478
- function readNumber(name, raw) {
31479
- if (raw === void 0 || raw.trim() === "") return null;
31480
- const n = Number(raw);
31481
- if (!Number.isFinite(n) || n < 0) {
31482
- throw new Error(`${name} must be a non-negative number (got ${JSON.stringify(raw)})`);
31483
- }
31484
- return n;
31485
- }
31486
- function parseDisabledClasses(raw) {
31487
- if (!raw || raw.trim() === "") return null;
31488
- const out = /* @__PURE__ */ new Set();
31489
- for (const part of raw.split(",")) {
31490
- const cls = part.trim();
31491
- if (!cls) continue;
31492
- if (!isAuthorityClass(cls)) {
31493
- throw new Error(
31494
- `LEMONSQUEEZY_DISABLE_CLASSES contains unknown class ${JSON.stringify(cls)} (expected one of: ${AUTHORITY_CLASSES.join(", ")})`
31495
- );
31496
- }
31497
- out.add(cls);
31498
- }
31499
- return out.size > 0 ? out : null;
31500
- }
31501
- function parseClassRateLimits(raw) {
31502
- if (!raw || raw.trim() === "") return null;
31503
- const out = /* @__PURE__ */ new Map();
31504
- for (const part of raw.split(",")) {
31505
- const segment = part.trim();
31506
- if (!segment) continue;
31507
- const colon = segment.indexOf(":");
31508
- if (colon < 0) {
31509
- throw new Error(
31510
- `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry missing colon: ${JSON.stringify(segment)} (expected class:N or class:N/h)`
31511
- );
31512
- }
31513
- const cls = segment.slice(0, colon).trim();
31514
- if (!isAuthorityClass(cls)) {
31515
- throw new Error(
31516
- `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS contains unknown class ${JSON.stringify(cls)} (expected one of: ${AUTHORITY_CLASSES.join(", ")})`
31517
- );
31518
- }
31519
- const specRaw = segment.slice(colon + 1).trim();
31520
- const slash = specRaw.indexOf("/");
31521
- const numPart = slash >= 0 ? specRaw.slice(0, slash).trim() : specRaw;
31522
- const unitPart = slash >= 0 ? specRaw.slice(slash + 1).trim().toLowerCase() : "m";
31523
- const n = Number(numPart);
31524
- if (!Number.isFinite(n) || n < 0 || numPart === "") {
31525
- throw new Error(`LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry has invalid number: ${JSON.stringify(segment)}`);
31526
- }
31527
- let windowMs;
31528
- if (unitPart === "m") windowMs = 6e4;
31529
- else if (unitPart === "h") windowMs = 36e5;
31530
- else {
31531
- throw new Error(
31532
- `LEMONSQUEEZY_RATE_LIMIT_PER_CLASS entry has invalid unit (expected m or h): ${JSON.stringify(segment)}`
31533
- );
31534
- }
31535
- out.set(cls, { limit: n, windowMs });
31536
- }
31537
- return out.size > 0 ? out : null;
31538
- }
31539
- function loadOptions() {
31540
- if (cachedOptions) return cachedOptions;
31541
- const allowed = process.env.LEMONSQUEEZY_ALLOWED_STORE_IDS;
31542
- const allowedSet = allowed ? new Set(
31543
- allowed.split(",").map((s) => s.trim()).filter(Boolean)
31544
- ) : null;
31545
- cachedOptions = {
31546
- allowedStoreIds: allowedSet && allowedSet.size > 0 ? allowedSet : null,
31547
- maxRefundAmountCents: readNumber(
31548
- "LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS",
31549
- process.env.LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS
31550
- ),
31551
- rateLimitPerMinute: readNumber(
31552
- "LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT",
31553
- process.env.LEMONSQUEEZY_DESTRUCTIVE_RATE_LIMIT
31554
- ),
31555
- disabledClasses: parseDisabledClasses(process.env.LEMONSQUEEZY_DISABLE_CLASSES),
31556
- classRateLimits: parseClassRateLimits(process.env.LEMONSQUEEZY_RATE_LIMIT_PER_CLASS)
31557
- };
31558
- return cachedOptions;
31559
- }
31560
- function checkStoreAllowed(storeId) {
31561
- if (!storeId) return;
31562
- const o = loadOptions();
31563
- if (!o.allowedStoreIds) return;
31564
- if (!o.allowedStoreIds.has(String(storeId))) {
31565
- throw new GuardrailError(`Store ID ${storeId} is not in LEMONSQUEEZY_ALLOWED_STORE_IDS allowlist`);
31566
- }
31567
- }
31568
- function checkRefundAmount(cents) {
31569
- const o = loadOptions();
31570
- if (o.maxRefundAmountCents === null) return;
31571
- if (cents > o.maxRefundAmountCents) {
31572
- throw new GuardrailError(
31573
- `Refund amount ${cents} cents exceeds LEMONSQUEEZY_MAX_REFUND_AMOUNT_CENTS (${o.maxRefundAmountCents})`
31574
- );
31575
- }
31576
- }
31577
- function checkDestructiveRateLimit(now = Date.now()) {
31578
- const o = loadOptions();
31579
- if (o.rateLimitPerMinute === null) return;
31580
- const cutoff = now - 6e4;
31581
- destructiveTimestamps = destructiveTimestamps.filter((t) => t > cutoff);
31582
- if (destructiveTimestamps.length >= o.rateLimitPerMinute) {
31583
- throw new GuardrailError(`Destructive call rate limit exceeded (${o.rateLimitPerMinute}/min). Wait and retry.`);
31584
- }
31585
- destructiveTimestamps.push(now);
31586
- }
31587
- function isStoreAllowlistActive() {
31588
- return loadOptions().allowedStoreIds !== null;
31589
- }
31590
- function isPresent(value) {
31591
- if (value === void 0 || value === null) return false;
31592
- if (typeof value === "string" && value === "") return false;
31593
- if (Array.isArray(value) && value.length === 0) return false;
31594
- return true;
31595
- }
31596
- function checkStoreScopedToolInput(tool, input) {
31597
- const toolAcceptsStoreId = "storeId" in tool.inputSchema.shape;
31598
- if (toolAcceptsStoreId) {
31599
- const raw = input.storeId;
31600
- if (raw !== void 0 && raw !== null && raw !== "") {
31601
- checkStoreAllowed(String(raw));
31602
- } else if (isStoreAllowlistActive()) {
31603
- throw new GuardrailError("storeId is required when LEMONSQUEEZY_ALLOWED_STORE_IDS is set");
31604
- }
31605
- }
31606
- if (tool.requiredFilters && tool.requiredFilters.length > 0 && isStoreAllowlistActive()) {
31607
- const anyPresent = tool.requiredFilters.some((key) => isPresent(input[key]));
31608
- if (!anyPresent) {
31609
- throw new GuardrailError(
31610
- `At least one of [${tool.requiredFilters.join(", ")}] is required when LEMONSQUEEZY_ALLOWED_STORE_IDS is set`
31611
- );
31612
- }
31613
- }
31614
- }
31615
- function isDestructiveCall(tool, input) {
31616
- if (typeof tool.isDestructive === "function") return tool.isDestructive(input);
31617
- return tool.annotations?.destructiveHint === true;
31618
- }
31619
- function checkClassAllowed(cls) {
31620
- const o = loadOptions();
31621
- if (!o.disabledClasses) return;
31622
- if (o.disabledClasses.has(cls)) {
31623
- throw new GuardrailError(`Tool authority class ${JSON.stringify(cls)} is disabled by LEMONSQUEEZY_DISABLE_CLASSES`);
31624
- }
31625
- }
31626
- function checkClassRateLimit(cls, now = Date.now()) {
31627
- const o = loadOptions();
31628
- if (!o.classRateLimits) return;
31629
- const spec = o.classRateLimits.get(cls);
31630
- if (!spec) return;
31631
- const cutoff = now - spec.windowMs;
31632
- const list = (classTimestamps.get(cls) ?? []).filter((t) => t > cutoff);
31633
- if (list.length >= spec.limit) {
31634
- const unit = spec.windowMs === 6e4 ? "min" : spec.windowMs === 36e5 ? "hour" : `${spec.windowMs}ms`;
31635
- classTimestamps.set(cls, list);
31636
- throw new GuardrailError(
31637
- `Class ${JSON.stringify(cls)} rate limit exceeded (${spec.limit}/${unit}). Wait and retry.`
31638
- );
31639
- }
31640
- list.push(now);
31641
- classTimestamps.set(cls, list);
31642
- }
31643
-
31644
31647
  // src/tools/orders.ts
31645
31648
  var orderTools = [
31646
31649
  {
@@ -31825,6 +31828,165 @@ var productTools = [
31825
31828
  }
31826
31829
  ];
31827
31830
 
31831
+ // src/tools/sink.ts
31832
+ var SINK_REPO_URL = "https://github.com/YawLabs/lemonsqueezy-webhook-sink";
31833
+ var FETCH_TIMEOUT_MS = 1e4;
31834
+ function loadSinkConfig() {
31835
+ const rawUrl = process.env.LEMONSQUEEZY_SINK_URL;
31836
+ const token = process.env.LEMONSQUEEZY_SINK_ADMIN_TOKEN;
31837
+ if (!rawUrl || !token) {
31838
+ const missing = [];
31839
+ if (!rawUrl) missing.push("LEMONSQUEEZY_SINK_URL");
31840
+ if (!token) missing.push("LEMONSQUEEZY_SINK_ADMIN_TOKEN");
31841
+ return {
31842
+ ok: false,
31843
+ error: `Sink not configured: ${missing.join(", ")} must be set. See ${SINK_REPO_URL} for setup.`
31844
+ };
31845
+ }
31846
+ return { url: rawUrl.replace(/\/+$/, ""), token };
31847
+ }
31848
+ function isToolHandlerResponse(value) {
31849
+ return "ok" in value;
31850
+ }
31851
+ function buildSinkPath(path, params) {
31852
+ const parts = [];
31853
+ for (const [k, v] of Object.entries(params)) {
31854
+ if (v === void 0 || v === null) continue;
31855
+ const s = String(v);
31856
+ if (s === "") continue;
31857
+ parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(s)}`);
31858
+ }
31859
+ const qs = parts.length > 0 ? `?${parts.join("&")}` : "";
31860
+ return `${path}${qs}`;
31861
+ }
31862
+ async function sinkRequest(config2, method, pathAndQuery) {
31863
+ const url2 = `${config2.url}${pathAndQuery}`;
31864
+ let res;
31865
+ try {
31866
+ res = await fetch(url2, {
31867
+ method,
31868
+ headers: {
31869
+ Authorization: `Bearer ${config2.token}`,
31870
+ Accept: "application/json"
31871
+ },
31872
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
31873
+ });
31874
+ } catch (err) {
31875
+ const message = err instanceof Error ? err.message : String(err);
31876
+ const isTimeout = err instanceof Error && (err.name === "TimeoutError" || /timeout/i.test(message));
31877
+ return {
31878
+ ok: false,
31879
+ error: isTimeout ? `Sink request timed out after ${Math.round(FETCH_TIMEOUT_MS / 1e3)}s (${config2.url})` : `Sink unreachable: ${message} (${config2.url})`
31880
+ };
31881
+ }
31882
+ if (!res.ok) {
31883
+ let body = "";
31884
+ try {
31885
+ body = await res.text();
31886
+ } catch {
31887
+ }
31888
+ let detail = body;
31889
+ try {
31890
+ const parsed = JSON.parse(body);
31891
+ if (parsed.error) detail = parsed.error;
31892
+ } catch {
31893
+ }
31894
+ if (res.status === 401) {
31895
+ return {
31896
+ ok: false,
31897
+ error: `Sink rejected admin token (401): ${detail || "unauthorized"}. Verify LEMONSQUEEZY_SINK_ADMIN_TOKEN matches the sink's WEBHOOK_SINK_ADMIN_TOKEN.`
31898
+ };
31899
+ }
31900
+ if (res.status === 404) {
31901
+ return {
31902
+ ok: false,
31903
+ error: `Sink admin endpoint not found (404): ${detail || "not found"}. The sink may have been started without WEBHOOK_SINK_ADMIN_TOKEN set, which disables admin endpoints.`
31904
+ };
31905
+ }
31906
+ return {
31907
+ ok: false,
31908
+ error: `Sink returned ${res.status}: ${detail || res.statusText}`
31909
+ };
31910
+ }
31911
+ const text = await res.text();
31912
+ if (!text.trim()) return { ok: true, data: {} };
31913
+ try {
31914
+ return { ok: true, data: JSON.parse(text) };
31915
+ } catch (err) {
31916
+ const message = err instanceof Error ? err.message : String(err);
31917
+ return { ok: false, error: `Sink returned invalid JSON: ${message}` };
31918
+ }
31919
+ }
31920
+ var sinkTools = [
31921
+ {
31922
+ name: "ls_sink_events_list",
31923
+ authorityClass: "read",
31924
+ description: "List webhook events the sink has received, optionally filtered. Use `since` (received_at timestamp, exclusive) to checkpoint. Requires the sink at LEMONSQUEEZY_SINK_URL with LEMONSQUEEZY_SINK_ADMIN_TOKEN.",
31925
+ annotations: {
31926
+ title: "List sink webhook events",
31927
+ readOnlyHint: true,
31928
+ destructiveHint: false,
31929
+ idempotentHint: true,
31930
+ openWorldHint: true
31931
+ },
31932
+ inputSchema: external_exports3.object({
31933
+ since: external_exports3.number().int().min(0).optional().describe(
31934
+ "Exclusive lower bound on received_at (Unix ms). Pass the highest received_at you have to checkpoint."
31935
+ ),
31936
+ type: external_exports3.string().max(200).optional().describe("Filter by event_name (e.g. 'order_created')."),
31937
+ limit: external_exports3.number().int().min(1).max(1e3).optional().describe("Maximum number of events to return.")
31938
+ }),
31939
+ handler: async (input) => {
31940
+ const config2 = loadSinkConfig();
31941
+ if (isToolHandlerResponse(config2)) return config2;
31942
+ const path = buildSinkPath("/events", {
31943
+ since: input.since,
31944
+ type: input.type,
31945
+ limit: input.limit
31946
+ });
31947
+ return sinkRequest(config2, "GET", path);
31948
+ }
31949
+ },
31950
+ {
31951
+ name: "ls_sink_event_mark_processed",
31952
+ authorityClass: "mutate",
31953
+ description: "Mark a sink event as processed by your consumer. Idempotent.",
31954
+ annotations: {
31955
+ title: "Mark sink event processed",
31956
+ readOnlyHint: false,
31957
+ destructiveHint: false,
31958
+ idempotentHint: true,
31959
+ openWorldHint: true
31960
+ },
31961
+ inputSchema: external_exports3.object({
31962
+ id: external_exports3.number().int().min(1).describe("The sink event ID (positive integer, as returned by ls_sink_events_list).")
31963
+ }),
31964
+ handler: async (input) => {
31965
+ const config2 = loadSinkConfig();
31966
+ if (isToolHandlerResponse(config2)) return config2;
31967
+ return sinkRequest(config2, "POST", `/events/${encodeURIComponent(String(input.id))}/processed`);
31968
+ }
31969
+ },
31970
+ {
31971
+ name: "ls_sink_stats",
31972
+ authorityClass: "read",
31973
+ description: "Get sink totals: total events, unprocessed count, last-received timestamp.",
31974
+ annotations: {
31975
+ title: "Get sink stats",
31976
+ readOnlyHint: true,
31977
+ destructiveHint: false,
31978
+ idempotentHint: true,
31979
+ openWorldHint: true
31980
+ },
31981
+ inputSchema: external_exports3.object({}),
31982
+ handler: async () => {
31983
+ const config2 = loadSinkConfig();
31984
+ if (isToolHandlerResponse(config2)) return config2;
31985
+ return sinkRequest(config2, "GET", "/stats");
31986
+ }
31987
+ }
31988
+ ];
31989
+
31828
31990
  // src/tools/stores.ts
31829
31991
  var storeTools = [
31830
31992
  {
@@ -32617,12 +32779,13 @@ function readAuditLogResource(uri) {
32617
32779
  }
32618
32780
 
32619
32781
  // src/index.ts
32620
- var version2 = true ? "0.9.3" : (await null).createRequire(import.meta.url)("../package.json").version;
32782
+ var version2 = true ? "0.10.1" : (await null).createRequire(import.meta.url)("../package.json").version;
32621
32783
  var subcommand = process.argv[2];
32622
32784
  if (subcommand === "version" || subcommand === "--version") {
32623
32785
  console.log(version2);
32624
32786
  process.exit(0);
32625
32787
  }
32788
+ loadGuardrailOptions();
32626
32789
  var allTools = [
32627
32790
  ...userTools,
32628
32791
  ...storeTools,
@@ -32644,7 +32807,8 @@ var allTools = [
32644
32807
  ...checkoutTools,
32645
32808
  ...webhookTools,
32646
32809
  ...licenseTools,
32647
- ...affiliateTools
32810
+ ...affiliateTools,
32811
+ ...sinkTools
32648
32812
  ];
32649
32813
  var server = new McpServer({
32650
32814
  name: "@yawlabs/lemonsqueezy-mcp",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/lemonsqueezy-mcp",
3
- "version": "0.9.3",
3
+ "version": "0.10.1",
4
4
  "mcpName": "io.github.YawLabs/lemonsqueezy-mcp",
5
5
  "description": "LemonSqueezy MCP server for managing your store from AI assistants",
6
6
  "license": "MIT",