@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.
- package/README.md +11 -2
- package/dist/index.js +162 -2
- 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 (
|
|
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.
|
|
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