@joy-loyalty/mcp 0.3.7

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 ADDED
@@ -0,0 +1,88 @@
1
+ # Joy Loyalty MCP Server
2
+
3
+ Connect Claude, Cursor, or any [MCP](https://modelcontextprotocol.io)-compatible AI client to your Joy Loyalty program. Ask questions and manage loyalty data in natural language — no code required.
4
+
5
+ ## Quick start
6
+
7
+ **1. Get your API key**
8
+
9
+ In your Joy admin, go to **Settings → Manage keys**:
10
+ - **Recommended:** click **"Generate read-only key"** (a `ro_...` key). This is the safest option — it can read your loyalty data but can never award, deduct, or change points, even if the key is ever exposed.
11
+ - Only use your full **Secret key** if you need write access (awarding points, adjusting balances, etc.).
12
+
13
+ **2. Add the server to your AI client**
14
+
15
+ For **Claude Desktop**, edit your config file:
16
+ - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
17
+ - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "joy-loyalty": {
23
+ "command": "npx",
24
+ "args": ["-y", "@joy-loyalty/mcp"],
25
+ "env": {
26
+ "JOY_APP_KEY": "<your-shop-id>",
27
+ "JOY_SECRET_KEY": "<your-key-from-step-1>"
28
+ }
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ No install required — `npx` downloads and runs the latest version automatically. [Node.js](https://nodejs.org) 20 or later must be installed on your machine.
35
+
36
+ **3. Restart your AI client** and try asking: *"How is my loyalty program set up?"*
37
+
38
+ ## Enabling write access (optional)
39
+
40
+ By default, the server is **read-only** — even if you provide your full secret key. To allow the assistant to award points, adjust balances, or make other changes, you must explicitly opt in:
41
+
42
+ ```json
43
+ "env": {
44
+ "JOY_APP_KEY": "<your-shop-id>",
45
+ "JOY_SECRET_KEY": "<your-full-secret-key>",
46
+ "JOY_MCP_ENABLE_WRITES": "true"
47
+ }
48
+ ```
49
+
50
+ ⚠️ **`"true"` must be quoted** — it's a text value, not a JSON boolean. Writing `true` without quotes will cause your AI client to reject the whole configuration with an error like *"not a valid MCP server configuration"*.
51
+
52
+ Your Joy plan must be **Advanced or higher** to use the API.
53
+
54
+ ## Environment variables
55
+
56
+ | Variable | Required | Description |
57
+ |---|---|---|
58
+ | `JOY_APP_KEY` | Yes | Your shop ID |
59
+ | `JOY_SECRET_KEY` | Yes (one of these two) | Your API key — a read-only (`ro_...`) or full secret key from Settings → Manage keys |
60
+ | `JOY_MCP_ENABLE_WRITES` | No | Set to `"true"` (quoted) to allow point/data changes. Requires a full secret key — read-only keys can never write regardless of this setting |
61
+ | `JOY_API_BASE_URL` | No | Defaults to production. Only change this if instructed by Joy support |
62
+
63
+ ## What it can do
64
+
65
+ **Always available (read-only):**
66
+ - Look up a member's points, tier, and profile
67
+ - List members and browse their point/reward/referral history
68
+ - View your shop's tiers, earning rules, and redemption rules
69
+ - View referral program performance
70
+
71
+ **With write access enabled:**
72
+ - Award or deduct points, with a mandatory reason recorded for every change
73
+ - Adjust a member's balance (always previews the change before applying it)
74
+ - Redeem points for a reward, refund a redeemed coupon, or change a member's tier
75
+ - Generate a referral invite link
76
+
77
+ Every point-changing action requires a stated reason and is recorded in your Joy activity log for audit purposes.
78
+
79
+ ## Security
80
+
81
+ - The server only talks to Joy's official API — it never sends your data anywhere else.
82
+ - Your API key is only sent to Joy's servers, over HTTPS.
83
+ - Read-only keys cannot perform any write operation, even if explicitly requested.
84
+ - Destructive actions (deducting points, refunds, tier changes) require your AI client's own confirmation step before running.
85
+
86
+ ## Support
87
+
88
+ Questions or issues: contact Joy support through your admin dashboard, or visit [joy.so](https://joy.so).
@@ -0,0 +1,55 @@
1
+ export class ConfigError extends Error {
2
+ }
3
+ const DEFAULT_API_BASE_URL = 'https://api.joy.so';
4
+ // Every request carries the shop's secret key in headers — a config pointing at
5
+ // an attacker's host would exfiltrate it on the first call. Only Joy API hosts
6
+ // (prod, staging, Firebase staging projects) and local emulators are accepted.
7
+ const JOY_API_HOSTS = new Set(['api.joy.so', 'dev-api.joy.so']);
8
+ const FIREBASE_STAGING_HOST = /^avada-joy(-staging(-\d+)?)?\.(firebaseapp\.com|web\.app)$/;
9
+ function assertAllowedBaseUrl(baseUrl) {
10
+ let parsed;
11
+ try {
12
+ parsed = new URL(baseUrl);
13
+ }
14
+ catch {
15
+ throw new ConfigError(`JOY_API_BASE_URL is not a valid URL: "${baseUrl}".`);
16
+ }
17
+ const host = parsed.hostname;
18
+ const isLocal = host === 'localhost' || host === '127.0.0.1';
19
+ // Non-local traffic must be TLS — the key travels in headers, and http://
20
+ // to a real host hands it to any on-path attacker.
21
+ if (!isLocal && parsed.protocol !== 'https:') {
22
+ throw new ConfigError(`JOY_API_BASE_URL must use https (got "${parsed.protocol}//") — the shop's secret key travels in request headers.`);
23
+ }
24
+ if (isLocal || JOY_API_HOSTS.has(host) || FIREBASE_STAGING_HOST.test(host))
25
+ return;
26
+ throw new ConfigError(`JOY_API_BASE_URL host "${host}" is not a Joy API host — refusing to send the shop's secret key there. Allowed: api.joy.so, dev-api.joy.so, avada-joy-staging-N.firebaseapp.com, localhost.`);
27
+ }
28
+ /**
29
+ * Load and validate configuration from environment variables. Fails fast with an
30
+ * actionable message — a misconfigured server must never reach the transport.
31
+ */
32
+ export function loadConfig(env = process.env) {
33
+ const appKey = env.JOY_APP_KEY?.trim();
34
+ const secretKey = env.JOY_SECRET_KEY?.trim() || undefined;
35
+ const privateBotKey = env.JOY_PRIVATE_BOT_KEY?.trim() || undefined;
36
+ const apiBaseUrl = (env.JOY_API_BASE_URL?.trim() || DEFAULT_API_BASE_URL).replace(/\/+$/, '');
37
+ assertAllowedBaseUrl(apiBaseUrl);
38
+ if (!appKey) {
39
+ throw new ConfigError('Missing JOY_APP_KEY (your shop ID). Set it in the "env" block of your MCP client config.');
40
+ }
41
+ if (!secretKey && !privateBotKey) {
42
+ throw new ConfigError('Missing credential: set JOY_SECRET_KEY (merchant API secret key) or JOY_PRIVATE_BOT_KEY (Joy-internal, read-only).');
43
+ }
44
+ if (secretKey && privateBotKey) {
45
+ throw new ConfigError('Set only ONE of JOY_SECRET_KEY or JOY_PRIVATE_BOT_KEY — they are mutually exclusive auth modes.');
46
+ }
47
+ const mode = privateBotKey ? 'internal-bot' : 'merchant';
48
+ // Writes require the merchant secret key AND an explicit opt-in flag. Having the
49
+ // secret key alone is not consent: it is the only key a merchant has.
50
+ const writesEnabled = mode === 'merchant' && env.JOY_MCP_ENABLE_WRITES === 'true';
51
+ // 0 is meaningful: "always dry-run every deduction" (most conservative)
52
+ const rawThreshold = Number(env.JOY_MCP_DEDUCT_DRYRUN_THRESHOLD ?? '1000');
53
+ const deductDryRunThreshold = Number.isFinite(rawThreshold) && rawThreshold >= 0 ? rawThreshold : 1000;
54
+ return { apiBaseUrl, appKey, secretKey, privateBotKey, mode, writesEnabled, deductDryRunThreshold };
55
+ }