@yawlabs/lemonsqueezy-mcp 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +11 -2
  2. package/dist/index.js +162 -2
  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
@@ -31825,6 +31825,165 @@ var productTools = [
31825
31825
  }
31826
31826
  ];
31827
31827
 
31828
+ // src/tools/sink.ts
31829
+ var SINK_REPO_URL = "https://github.com/YawLabs/lemonsqueezy-webhook-sink";
31830
+ var FETCH_TIMEOUT_MS = 1e4;
31831
+ function loadSinkConfig() {
31832
+ const rawUrl = process.env.LEMONSQUEEZY_SINK_URL;
31833
+ const token = process.env.LEMONSQUEEZY_SINK_ADMIN_TOKEN;
31834
+ if (!rawUrl || !token) {
31835
+ const missing = [];
31836
+ if (!rawUrl) missing.push("LEMONSQUEEZY_SINK_URL");
31837
+ if (!token) missing.push("LEMONSQUEEZY_SINK_ADMIN_TOKEN");
31838
+ return {
31839
+ ok: false,
31840
+ error: `Sink not configured: ${missing.join(", ")} must be set. See ${SINK_REPO_URL} for setup.`
31841
+ };
31842
+ }
31843
+ return { url: rawUrl.replace(/\/+$/, ""), token };
31844
+ }
31845
+ function isToolHandlerResponse(value) {
31846
+ return "ok" in value;
31847
+ }
31848
+ function buildSinkPath(path, params) {
31849
+ const parts = [];
31850
+ for (const [k, v] of Object.entries(params)) {
31851
+ if (v === void 0 || v === null) continue;
31852
+ const s = String(v);
31853
+ if (s === "") continue;
31854
+ parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(s)}`);
31855
+ }
31856
+ const qs = parts.length > 0 ? `?${parts.join("&")}` : "";
31857
+ return `${path}${qs}`;
31858
+ }
31859
+ async function sinkRequest(config2, method, pathAndQuery) {
31860
+ const url2 = `${config2.url}${pathAndQuery}`;
31861
+ let res;
31862
+ try {
31863
+ res = await fetch(url2, {
31864
+ method,
31865
+ headers: {
31866
+ Authorization: `Bearer ${config2.token}`,
31867
+ Accept: "application/json"
31868
+ },
31869
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
31870
+ });
31871
+ } catch (err) {
31872
+ const message = err instanceof Error ? err.message : String(err);
31873
+ const isTimeout = err instanceof Error && (err.name === "TimeoutError" || /timeout/i.test(message));
31874
+ return {
31875
+ ok: false,
31876
+ error: isTimeout ? `Sink request timed out after ${Math.round(FETCH_TIMEOUT_MS / 1e3)}s (${config2.url})` : `Sink unreachable: ${message} (${config2.url})`
31877
+ };
31878
+ }
31879
+ if (!res.ok) {
31880
+ let body = "";
31881
+ try {
31882
+ body = await res.text();
31883
+ } catch {
31884
+ }
31885
+ let detail = body;
31886
+ try {
31887
+ const parsed = JSON.parse(body);
31888
+ if (parsed.error) detail = parsed.error;
31889
+ } catch {
31890
+ }
31891
+ if (res.status === 401) {
31892
+ return {
31893
+ ok: false,
31894
+ error: `Sink rejected admin token (401): ${detail || "unauthorized"}. Verify LEMONSQUEEZY_SINK_ADMIN_TOKEN matches the sink's WEBHOOK_SINK_ADMIN_TOKEN.`
31895
+ };
31896
+ }
31897
+ if (res.status === 404) {
31898
+ return {
31899
+ ok: false,
31900
+ 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.`
31901
+ };
31902
+ }
31903
+ return {
31904
+ ok: false,
31905
+ error: `Sink returned ${res.status}: ${detail || res.statusText}`
31906
+ };
31907
+ }
31908
+ const text = await res.text();
31909
+ if (!text.trim()) return { ok: true, data: {} };
31910
+ try {
31911
+ return { ok: true, data: JSON.parse(text) };
31912
+ } catch (err) {
31913
+ const message = err instanceof Error ? err.message : String(err);
31914
+ return { ok: false, error: `Sink returned invalid JSON: ${message}` };
31915
+ }
31916
+ }
31917
+ var sinkTools = [
31918
+ {
31919
+ name: "ls_sink_events_list",
31920
+ authorityClass: "read",
31921
+ 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.",
31922
+ annotations: {
31923
+ title: "List sink webhook events",
31924
+ readOnlyHint: true,
31925
+ destructiveHint: false,
31926
+ idempotentHint: true,
31927
+ openWorldHint: true
31928
+ },
31929
+ inputSchema: external_exports3.object({
31930
+ since: external_exports3.number().int().min(0).optional().describe(
31931
+ "Exclusive lower bound on received_at (Unix ms). Pass the highest received_at you have to checkpoint."
31932
+ ),
31933
+ type: external_exports3.string().max(200).optional().describe("Filter by event_name (e.g. 'order_created')."),
31934
+ limit: external_exports3.number().int().min(1).max(1e3).optional().describe("Maximum number of events to return.")
31935
+ }),
31936
+ handler: async (input) => {
31937
+ const config2 = loadSinkConfig();
31938
+ if (isToolHandlerResponse(config2)) return config2;
31939
+ const path = buildSinkPath("/events", {
31940
+ since: input.since,
31941
+ type: input.type,
31942
+ limit: input.limit
31943
+ });
31944
+ return sinkRequest(config2, "GET", path);
31945
+ }
31946
+ },
31947
+ {
31948
+ name: "ls_sink_event_mark_processed",
31949
+ authorityClass: "mutate",
31950
+ description: "Mark a sink event as processed by your consumer. Idempotent.",
31951
+ annotations: {
31952
+ title: "Mark sink event processed",
31953
+ readOnlyHint: false,
31954
+ destructiveHint: false,
31955
+ idempotentHint: true,
31956
+ openWorldHint: true
31957
+ },
31958
+ inputSchema: external_exports3.object({
31959
+ id: external_exports3.number().int().min(1).describe("The sink event ID (positive integer, as returned by ls_sink_events_list).")
31960
+ }),
31961
+ handler: async (input) => {
31962
+ const config2 = loadSinkConfig();
31963
+ if (isToolHandlerResponse(config2)) return config2;
31964
+ return sinkRequest(config2, "POST", `/events/${encodeURIComponent(String(input.id))}/processed`);
31965
+ }
31966
+ },
31967
+ {
31968
+ name: "ls_sink_stats",
31969
+ authorityClass: "read",
31970
+ description: "Get sink totals: total events, unprocessed count, last-received timestamp.",
31971
+ annotations: {
31972
+ title: "Get sink stats",
31973
+ readOnlyHint: true,
31974
+ destructiveHint: false,
31975
+ idempotentHint: true,
31976
+ openWorldHint: true
31977
+ },
31978
+ inputSchema: external_exports3.object({}),
31979
+ handler: async () => {
31980
+ const config2 = loadSinkConfig();
31981
+ if (isToolHandlerResponse(config2)) return config2;
31982
+ return sinkRequest(config2, "GET", "/stats");
31983
+ }
31984
+ }
31985
+ ];
31986
+
31828
31987
  // src/tools/stores.ts
31829
31988
  var storeTools = [
31830
31989
  {
@@ -32617,7 +32776,7 @@ function readAuditLogResource(uri) {
32617
32776
  }
32618
32777
 
32619
32778
  // src/index.ts
32620
- var version2 = true ? "0.9.2" : (await null).createRequire(import.meta.url)("../package.json").version;
32779
+ var version2 = true ? "0.10.0" : (await null).createRequire(import.meta.url)("../package.json").version;
32621
32780
  var subcommand = process.argv[2];
32622
32781
  if (subcommand === "version" || subcommand === "--version") {
32623
32782
  console.log(version2);
@@ -32644,7 +32803,8 @@ var allTools = [
32644
32803
  ...checkoutTools,
32645
32804
  ...webhookTools,
32646
32805
  ...licenseTools,
32647
- ...affiliateTools
32806
+ ...affiliateTools,
32807
+ ...sinkTools
32648
32808
  ];
32649
32809
  var server = new McpServer({
32650
32810
  name: "@yawlabs/lemonsqueezy-mcp",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/lemonsqueezy-mcp",
3
- "version": "0.9.2",
3
+ "version": "0.10.0",
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",