@storelayer/mcp-server 0.3.0 → 0.4.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/dist/index.js +180 -49
- package/package.json +2 -3
- package/SKILL.md +0 -482
package/dist/index.js
CHANGED
|
@@ -6516,11 +6516,6 @@ var require_dist = __commonJS((exports, module) => {
|
|
|
6516
6516
|
exports.default = formatsPlugin;
|
|
6517
6517
|
});
|
|
6518
6518
|
|
|
6519
|
-
// src/index.ts
|
|
6520
|
-
import { readFileSync } from "node:fs";
|
|
6521
|
-
import { resolve, dirname } from "node:path";
|
|
6522
|
-
import { fileURLToPath } from "node:url";
|
|
6523
|
-
|
|
6524
6519
|
// ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/core/core.js
|
|
6525
6520
|
var NEVER = Object.freeze({
|
|
6526
6521
|
status: "aborted"
|
|
@@ -13661,16 +13656,92 @@ class StdioServerTransport {
|
|
|
13661
13656
|
}
|
|
13662
13657
|
|
|
13663
13658
|
// src/index.ts
|
|
13664
|
-
var __dirname2 = dirname(fileURLToPath(import.meta.url));
|
|
13665
|
-
var FALLBACK_SKILL_CONTENT;
|
|
13666
|
-
try {
|
|
13667
|
-
FALLBACK_SKILL_CONTENT = readFileSync(resolve(__dirname2, "../SKILL.md"), "utf-8");
|
|
13668
|
-
} catch {
|
|
13669
|
-
FALLBACK_SKILL_CONTENT = "Store Layer MCP Server — manage loyalty programs, promotions, wallets, users, events, and referrals. Use the available tools to interact with your project.";
|
|
13670
|
-
}
|
|
13671
13659
|
var API_URL = (process.env.STORE_LAYER_API_URL || "https://api.storelayer.io").replace(/\/$/, "");
|
|
13672
13660
|
var API_KEY = process.env.STORE_LAYER_API_KEY;
|
|
13673
13661
|
var PROJECT_ID = process.env.STORE_LAYER_PROJECT_ID;
|
|
13662
|
+
var SKILLS_REPO = "storelayer/skills";
|
|
13663
|
+
var SKILLS_BASE_PATH = "skills/storelayer";
|
|
13664
|
+
var SKILL_REFERENCES = [
|
|
13665
|
+
{
|
|
13666
|
+
name: "architecture",
|
|
13667
|
+
description: "Platform architecture and patterns",
|
|
13668
|
+
path: "references/architecture.md"
|
|
13669
|
+
},
|
|
13670
|
+
{
|
|
13671
|
+
name: "wallet",
|
|
13672
|
+
description: "Wallet/ledger system reference",
|
|
13673
|
+
path: "references/wallet.md"
|
|
13674
|
+
},
|
|
13675
|
+
{
|
|
13676
|
+
name: "promotions",
|
|
13677
|
+
description: "Promotion engine reference",
|
|
13678
|
+
path: "references/promotions.md"
|
|
13679
|
+
},
|
|
13680
|
+
{
|
|
13681
|
+
name: "referral",
|
|
13682
|
+
description: "Referral system reference",
|
|
13683
|
+
path: "references/referral.md"
|
|
13684
|
+
},
|
|
13685
|
+
{
|
|
13686
|
+
name: "events",
|
|
13687
|
+
description: "Events and rules reference",
|
|
13688
|
+
path: "references/events.md"
|
|
13689
|
+
},
|
|
13690
|
+
{
|
|
13691
|
+
name: "external-users",
|
|
13692
|
+
description: "Customer management reference",
|
|
13693
|
+
path: "references/external-users.md"
|
|
13694
|
+
},
|
|
13695
|
+
{
|
|
13696
|
+
name: "discount-scripts",
|
|
13697
|
+
description: "Discount script reference",
|
|
13698
|
+
path: "references/discount-scripts.md"
|
|
13699
|
+
},
|
|
13700
|
+
{
|
|
13701
|
+
name: "conditions",
|
|
13702
|
+
description: "Condition engine reference",
|
|
13703
|
+
path: "references/conditions.md"
|
|
13704
|
+
},
|
|
13705
|
+
{
|
|
13706
|
+
name: "mcp-tools",
|
|
13707
|
+
description: "All MCP tools reference",
|
|
13708
|
+
path: "references/mcp-tools.md"
|
|
13709
|
+
}
|
|
13710
|
+
];
|
|
13711
|
+
var SKILL_AGENTS = [
|
|
13712
|
+
{
|
|
13713
|
+
name: "loyalty-builder",
|
|
13714
|
+
description: "Design and build complete loyalty programs",
|
|
13715
|
+
path: "agents/loyalty-builder.md"
|
|
13716
|
+
},
|
|
13717
|
+
{
|
|
13718
|
+
name: "promo-engineer",
|
|
13719
|
+
description: "Build promotions, discount scripts, and coupon campaigns",
|
|
13720
|
+
path: "agents/promo-engineer.md"
|
|
13721
|
+
},
|
|
13722
|
+
{
|
|
13723
|
+
name: "integration-dev",
|
|
13724
|
+
description: "Set up event ingestion and external integrations",
|
|
13725
|
+
path: "agents/integration-dev.md"
|
|
13726
|
+
}
|
|
13727
|
+
];
|
|
13728
|
+
var SKILL_TOOLS = [
|
|
13729
|
+
{
|
|
13730
|
+
name: "setup-project",
|
|
13731
|
+
description: "Step-by-step new project setup",
|
|
13732
|
+
path: "tools/setup-project.md"
|
|
13733
|
+
},
|
|
13734
|
+
{
|
|
13735
|
+
name: "test-promotion",
|
|
13736
|
+
description: "Test promotions before activation",
|
|
13737
|
+
path: "tools/test-promotion.md"
|
|
13738
|
+
},
|
|
13739
|
+
{
|
|
13740
|
+
name: "debug-rules",
|
|
13741
|
+
description: "Debug rules and promotions",
|
|
13742
|
+
path: "tools/debug-rules.md"
|
|
13743
|
+
}
|
|
13744
|
+
];
|
|
13674
13745
|
function apiHeaders() {
|
|
13675
13746
|
return {
|
|
13676
13747
|
Authorization: `Bearer ${API_KEY}`,
|
|
@@ -13687,24 +13758,6 @@ async function fetchToolManifest() {
|
|
|
13687
13758
|
const json = await res.json();
|
|
13688
13759
|
return json.data.tools;
|
|
13689
13760
|
}
|
|
13690
|
-
async function fetchSkillContent() {
|
|
13691
|
-
try {
|
|
13692
|
-
const url = `${API_URL}/projects/${PROJECT_ID}/internal-skill`;
|
|
13693
|
-
const res = await fetch(url, { headers: apiHeaders() });
|
|
13694
|
-
if (!res.ok) {
|
|
13695
|
-
console.error(`Skill content fetch failed (${res.status}), using fallback`);
|
|
13696
|
-
return { content: FALLBACK_SKILL_CONTENT, version: "local" };
|
|
13697
|
-
}
|
|
13698
|
-
const json = await res.json();
|
|
13699
|
-
if (json.success && json.data?.content) {
|
|
13700
|
-
return { content: json.data.content, version: json.data.version };
|
|
13701
|
-
}
|
|
13702
|
-
return { content: FALLBACK_SKILL_CONTENT, version: "local" };
|
|
13703
|
-
} catch (error2) {
|
|
13704
|
-
console.error("Failed to fetch skill content, using fallback:", error2);
|
|
13705
|
-
return { content: FALLBACK_SKILL_CONTENT, version: "local" };
|
|
13706
|
-
}
|
|
13707
|
-
}
|
|
13708
13761
|
async function executeTool(toolName, params, userId) {
|
|
13709
13762
|
const url = `${API_URL}/projects/${PROJECT_ID}/internal-tools/${toolName}/execute`;
|
|
13710
13763
|
const body = { params };
|
|
@@ -13724,6 +13777,31 @@ async function executeTool(toolName, params, userId) {
|
|
|
13724
13777
|
throw new Error(json.error || "Tool execution failed");
|
|
13725
13778
|
return json.data;
|
|
13726
13779
|
}
|
|
13780
|
+
var contentCache = new Map;
|
|
13781
|
+
var CACHE_TTL_MS = 15 * 60 * 1000;
|
|
13782
|
+
async function fetchSkillFile(relativePath) {
|
|
13783
|
+
const cacheKey = relativePath;
|
|
13784
|
+
const cached2 = contentCache.get(cacheKey);
|
|
13785
|
+
if (cached2 && Date.now() - cached2.fetchedAt < CACHE_TTL_MS) {
|
|
13786
|
+
return cached2.content;
|
|
13787
|
+
}
|
|
13788
|
+
const url = `https://raw.githubusercontent.com/${SKILLS_REPO}/main/${SKILLS_BASE_PATH}/${relativePath}`;
|
|
13789
|
+
const res = await fetch(url);
|
|
13790
|
+
if (!res.ok) {
|
|
13791
|
+
throw new Error(`Failed to fetch ${relativePath} from GitHub (${res.status})`);
|
|
13792
|
+
}
|
|
13793
|
+
const content = await res.text();
|
|
13794
|
+
contentCache.set(cacheKey, { content, fetchedAt: Date.now() });
|
|
13795
|
+
return content;
|
|
13796
|
+
}
|
|
13797
|
+
async function fetchSkillGuide() {
|
|
13798
|
+
try {
|
|
13799
|
+
return await fetchSkillFile("SKILL.md");
|
|
13800
|
+
} catch (error2) {
|
|
13801
|
+
console.error("Failed to fetch SKILL.md from GitHub:", error2);
|
|
13802
|
+
return "Storelayer MCP Server — manage loyalty programs, promotions, wallets, users, events, and referrals. Use the available tools to interact with your project. Skill guide unavailable (GitHub unreachable).";
|
|
13803
|
+
}
|
|
13804
|
+
}
|
|
13727
13805
|
function buildInputSchema(tool) {
|
|
13728
13806
|
const schema = structuredClone(tool.parameters);
|
|
13729
13807
|
if (!schema.type)
|
|
@@ -13757,12 +13835,12 @@ async function main() {
|
|
|
13757
13835
|
process.exit(1);
|
|
13758
13836
|
}
|
|
13759
13837
|
console.error(`Connecting to Store Layer API at ${API_URL}...`);
|
|
13760
|
-
const [manifest,
|
|
13838
|
+
const [manifest, skillGuide] = await Promise.all([
|
|
13761
13839
|
fetchToolManifest(),
|
|
13762
|
-
|
|
13840
|
+
fetchSkillGuide()
|
|
13763
13841
|
]);
|
|
13764
13842
|
console.error(`Loaded ${manifest.length} tools from Store Layer`);
|
|
13765
|
-
console.error(`Skill guide
|
|
13843
|
+
console.error(`Skill guide loaded from GitHub (${SKILLS_REPO})`);
|
|
13766
13844
|
const mcpToRegistry = new Map;
|
|
13767
13845
|
const mcpToManifest = new Map;
|
|
13768
13846
|
for (const tool of manifest) {
|
|
@@ -13770,7 +13848,12 @@ async function main() {
|
|
|
13770
13848
|
mcpToRegistry.set(mcpName, tool.name);
|
|
13771
13849
|
mcpToManifest.set(mcpName, tool);
|
|
13772
13850
|
}
|
|
13773
|
-
const
|
|
13851
|
+
const allSkillFiles = [
|
|
13852
|
+
...SKILL_REFERENCES.map((f) => ({ ...f, category: "reference" })),
|
|
13853
|
+
...SKILL_AGENTS.map((f) => ({ ...f, category: "agent" })),
|
|
13854
|
+
...SKILL_TOOLS.map((f) => ({ ...f, category: "tool" }))
|
|
13855
|
+
];
|
|
13856
|
+
const server = new Server({ name: "store-layer", version: "0.4.0" }, { capabilities: { tools: {}, prompts: {}, resources: {} } });
|
|
13774
13857
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
13775
13858
|
tools: manifest.map((tool) => ({
|
|
13776
13859
|
name: toMcpName(tool.name),
|
|
@@ -13821,26 +13904,74 @@ async function main() {
|
|
|
13821
13904
|
prompts: [
|
|
13822
13905
|
{
|
|
13823
13906
|
name: "store-layer-guide",
|
|
13824
|
-
description:
|
|
13825
|
-
}
|
|
13907
|
+
description: "Comprehensive guide for using Storelayer tools — covers all domains (promotions, wallets, users, events, referrals, rules), with recipes and best practices."
|
|
13908
|
+
},
|
|
13909
|
+
...SKILL_AGENTS.map((agent) => ({
|
|
13910
|
+
name: agent.name,
|
|
13911
|
+
description: agent.description
|
|
13912
|
+
}))
|
|
13826
13913
|
]
|
|
13827
13914
|
}));
|
|
13828
13915
|
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
13829
|
-
|
|
13830
|
-
|
|
13916
|
+
const promptName = request.params.name;
|
|
13917
|
+
if (promptName === "store-layer-guide") {
|
|
13918
|
+
return {
|
|
13919
|
+
description: "Storelayer MCP skill guide",
|
|
13920
|
+
messages: [
|
|
13921
|
+
{
|
|
13922
|
+
role: "user",
|
|
13923
|
+
content: { type: "text", text: skillGuide }
|
|
13924
|
+
}
|
|
13925
|
+
]
|
|
13926
|
+
};
|
|
13831
13927
|
}
|
|
13832
|
-
|
|
13833
|
-
|
|
13834
|
-
|
|
13835
|
-
|
|
13836
|
-
|
|
13837
|
-
|
|
13838
|
-
|
|
13839
|
-
|
|
13928
|
+
const agent = SKILL_AGENTS.find((a) => a.name === promptName);
|
|
13929
|
+
if (agent) {
|
|
13930
|
+
try {
|
|
13931
|
+
const content = await fetchSkillFile(agent.path);
|
|
13932
|
+
return {
|
|
13933
|
+
description: agent.description,
|
|
13934
|
+
messages: [
|
|
13935
|
+
{
|
|
13936
|
+
role: "user",
|
|
13937
|
+
content: { type: "text", text: content }
|
|
13938
|
+
}
|
|
13939
|
+
]
|
|
13940
|
+
};
|
|
13941
|
+
} catch (error2) {
|
|
13942
|
+
throw new Error(`Failed to fetch agent ${agent.name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
13943
|
+
}
|
|
13944
|
+
}
|
|
13945
|
+
throw new Error(`Unknown prompt: ${promptName}`);
|
|
13946
|
+
});
|
|
13947
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
13948
|
+
resources: allSkillFiles.map((file) => ({
|
|
13949
|
+
uri: `storelayer://${file.category}/${file.name}`,
|
|
13950
|
+
name: file.name,
|
|
13951
|
+
description: file.description,
|
|
13952
|
+
mimeType: "text/markdown"
|
|
13953
|
+
}))
|
|
13954
|
+
}));
|
|
13955
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
13956
|
+
const uri = request.params.uri;
|
|
13957
|
+
const file = allSkillFiles.find((f) => `storelayer://${f.category}/${f.name}` === uri);
|
|
13958
|
+
if (!file) {
|
|
13959
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
13960
|
+
}
|
|
13961
|
+
try {
|
|
13962
|
+
const content = await fetchSkillFile(file.path);
|
|
13963
|
+
return {
|
|
13964
|
+
contents: [
|
|
13965
|
+
{
|
|
13966
|
+
uri,
|
|
13967
|
+
mimeType: "text/markdown",
|
|
13968
|
+
text: content
|
|
13840
13969
|
}
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
}
|
|
13970
|
+
]
|
|
13971
|
+
};
|
|
13972
|
+
} catch (error2) {
|
|
13973
|
+
throw new Error(`Failed to fetch ${file.path}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
13974
|
+
}
|
|
13844
13975
|
});
|
|
13845
13976
|
const transport = new StdioServerTransport;
|
|
13846
13977
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storelayer/mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "MCP server for Store Layer — manage loyalty programs, promotions, wallets, and more from Claude Desktop, Claude Code, or Cursor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"store-layer-mcp-server": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"dist"
|
|
11
|
-
"SKILL.md"
|
|
10
|
+
"dist"
|
|
12
11
|
],
|
|
13
12
|
"scripts": {
|
|
14
13
|
"build": "bun build src/index.ts --target=node --outdir=dist --format=esm",
|
package/SKILL.md
DELETED
|
@@ -1,482 +0,0 @@
|
|
|
1
|
-
# Store Layer — AI Skill Guide
|
|
2
|
-
|
|
3
|
-
You have access to the **Store Layer** MCP server. This guide helps you build loyalty programs, promotions, and customer engagement systems effectively.
|
|
4
|
-
|
|
5
|
-
## Pipeline: How to Build Loyalty Programs
|
|
6
|
-
|
|
7
|
-
**Always follow this pipeline** when building loyalty features. Do not skip steps.
|
|
8
|
-
|
|
9
|
-
### Step 1: Discover Current State
|
|
10
|
-
|
|
11
|
-
Before creating anything, understand what exists:
|
|
12
|
-
|
|
13
|
-
```
|
|
14
|
-
1. project_get_config → currency, timezone, cart format
|
|
15
|
-
2. project_list_rules → existing loyalty rules
|
|
16
|
-
3. promotions_get_active → active promotions
|
|
17
|
-
4. events_get_stats → event flow (what types come in)
|
|
18
|
-
5. wallet_get_balance (userId) → existing asset types in use
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
### Step 2: Design the Program
|
|
22
|
-
|
|
23
|
-
Map the user's intent to domains:
|
|
24
|
-
|
|
25
|
-
| User wants... | Domains involved |
|
|
26
|
-
| ---------------------------- | ------------------------------- |
|
|
27
|
-
| "Earn points on purchase" | Rules + Wallet |
|
|
28
|
-
| "10% off orders over $50" | Promotions |
|
|
29
|
-
| "Buy 2 get 1 free" | Promotions (script) |
|
|
30
|
-
| "Refer a friend, get $10" | Referral + Wallet |
|
|
31
|
-
| "Double points this weekend" | Rules (with date conditions) |
|
|
32
|
-
| "Coupon code for 20% off" | Promotions + Coupons |
|
|
33
|
-
| "VIP tier discounts" | Promotions (conditions on user) |
|
|
34
|
-
|
|
35
|
-
**Propose the full plan before creating anything.** Show the user what you'll create.
|
|
36
|
-
|
|
37
|
-
### Step 3: Build & Test Incrementally
|
|
38
|
-
|
|
39
|
-
For **rules**: draft conditions → test with `project_test_conditions` → create rule
|
|
40
|
-
For **promotions**: create as `draft` → test with `promotions_evaluate_cart` → activate
|
|
41
|
-
For **coupons**: create coupon → test cart with coupon code → verify
|
|
42
|
-
|
|
43
|
-
### Step 4: Verify End-to-End
|
|
44
|
-
|
|
45
|
-
After building, verify the chain works:
|
|
46
|
-
|
|
47
|
-
- For rules: event type → conditions → actions → wallet credit
|
|
48
|
-
- For promotions: cart → conditions → script → discounts
|
|
49
|
-
- Summarize what was built and how pieces connect
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
## Recipes
|
|
54
|
-
|
|
55
|
-
### Recipe 1: Points-for-Purchase (Earn 1 point per dollar)
|
|
56
|
-
|
|
57
|
-
**Domains:** Rules + Wallet
|
|
58
|
-
|
|
59
|
-
**Step 1:** Create the rule:
|
|
60
|
-
|
|
61
|
-
```json
|
|
62
|
-
project_add_rule({
|
|
63
|
-
"name": "1 Point Per Dollar",
|
|
64
|
-
"conditions": {
|
|
65
|
-
"conditions": [
|
|
66
|
-
{ "leftValue": "{{ event.type }}", "operator": "equals", "rightValue": "purchase", "rightType": "string" }
|
|
67
|
-
],
|
|
68
|
-
"combinator": "AND"
|
|
69
|
-
},
|
|
70
|
-
"actions": [
|
|
71
|
-
{
|
|
72
|
-
"type": "reward",
|
|
73
|
-
"config": {
|
|
74
|
-
"assetType": "points",
|
|
75
|
-
"amount": "{{ event.payload.amount }}",
|
|
76
|
-
"description": "Purchase reward: {{ event.payload.amount }} points"
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
],
|
|
80
|
-
"resources": {
|
|
81
|
-
"event": { "type": "purchase" }
|
|
82
|
-
}
|
|
83
|
-
})
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
**Step 2:** Test conditions:
|
|
87
|
-
|
|
88
|
-
```json
|
|
89
|
-
project_test_conditions({
|
|
90
|
-
"conditions": {
|
|
91
|
-
"conditions": [
|
|
92
|
-
{ "leftValue": "{{ event.type }}", "operator": "equals", "rightValue": "purchase", "rightType": "string" }
|
|
93
|
-
],
|
|
94
|
-
"combinator": "AND"
|
|
95
|
-
},
|
|
96
|
-
"context": {
|
|
97
|
-
"event": { "type": "purchase", "payload": { "amount": 49.99 } }
|
|
98
|
-
}
|
|
99
|
-
})
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
**Step 3:** Verify wallet (for a test user):
|
|
103
|
-
|
|
104
|
-
```json
|
|
105
|
-
wallet_get_balance({ "userId": "test-user-123" })
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
### Recipe 2: Percentage Discount (10% off everything)
|
|
111
|
-
|
|
112
|
-
**Domains:** Promotions
|
|
113
|
-
|
|
114
|
-
**Step 1:** Create promotion in draft:
|
|
115
|
-
|
|
116
|
-
```json
|
|
117
|
-
promotions_create({
|
|
118
|
-
"name": "10% Off Everything",
|
|
119
|
-
"status": "draft",
|
|
120
|
-
"conditions": { "conditions": [], "combinator": "AND" },
|
|
121
|
-
"itemsDiscountComputation": {
|
|
122
|
-
"script": "const items = $('cart').items;\nreturn items.map(item => ({ id: item.id, amount: item.price * 0.10 }));",
|
|
123
|
-
"language": "javascript"
|
|
124
|
-
}
|
|
125
|
-
})
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
**Step 2:** Test with a cart:
|
|
129
|
-
|
|
130
|
-
```json
|
|
131
|
-
promotions_evaluate_cart({
|
|
132
|
-
"cart": {
|
|
133
|
-
"items": [
|
|
134
|
-
{ "id": "item-1", "price": 25.00, "quantity": 1 },
|
|
135
|
-
{ "id": "item-2", "price": 15.00, "quantity": 2 }
|
|
136
|
-
]
|
|
137
|
-
}
|
|
138
|
-
})
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
**Step 3:** Check results — verify `summary.totalDiscount` is 10% of total. If correct, activate:
|
|
142
|
-
|
|
143
|
-
```json
|
|
144
|
-
promotions_update({ "promotionId": "promo_xxx", "status": "active" })
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
---
|
|
148
|
-
|
|
149
|
-
### Recipe 3: Threshold Discount ($5 off orders over $50)
|
|
150
|
-
|
|
151
|
-
**Domains:** Promotions
|
|
152
|
-
|
|
153
|
-
```json
|
|
154
|
-
promotions_create({
|
|
155
|
-
"name": "$5 Off Orders Over $50",
|
|
156
|
-
"status": "draft",
|
|
157
|
-
"conditions": {
|
|
158
|
-
"conditions": [
|
|
159
|
-
{ "leftValue": "cart.total", "operator": "gte", "rightValue": 50, "rightType": "number" }
|
|
160
|
-
],
|
|
161
|
-
"combinator": "AND"
|
|
162
|
-
},
|
|
163
|
-
"itemsDiscountComputation": {
|
|
164
|
-
"script": "const items = $('cart').items;\nconst total = items.reduce((s, i) => s + i.price, 0);\nreturn items.map(item => ({ id: item.id, amount: (item.price / total) * 5 }));",
|
|
165
|
-
"language": "javascript"
|
|
166
|
-
}
|
|
167
|
-
})
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
**Key:** The script distributes the $5 proportionally across items by their price share.
|
|
171
|
-
|
|
172
|
-
---
|
|
173
|
-
|
|
174
|
-
### Recipe 4: BOGO (Buy 2+ shoes, cheapest free)
|
|
175
|
-
|
|
176
|
-
**Domains:** Promotions
|
|
177
|
-
|
|
178
|
-
```json
|
|
179
|
-
promotions_create({
|
|
180
|
-
"name": "BOGO Shoes",
|
|
181
|
-
"status": "draft",
|
|
182
|
-
"conditions": { "conditions": [], "combinator": "AND" },
|
|
183
|
-
"itemsDiscountComputation": {
|
|
184
|
-
"script": "const shoes = $('cart').items.filter(i => i.category === 'shoes');\nif (shoes.length < 2) return [];\nconst sorted = [...shoes].sort((a, b) => a.price - b.price);\nreturn [{ id: sorted[0].id, amount: sorted[0].price }];",
|
|
185
|
-
"language": "javascript"
|
|
186
|
-
}
|
|
187
|
-
})
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
**Test cart must include `category` field on items**, or the filter returns nothing.
|
|
191
|
-
|
|
192
|
-
---
|
|
193
|
-
|
|
194
|
-
### Recipe 5: Coupon Code (20% off with code SAVE20)
|
|
195
|
-
|
|
196
|
-
**Domains:** Promotions + Coupons
|
|
197
|
-
|
|
198
|
-
**Step 1:** Create promotion requiring coupon:
|
|
199
|
-
|
|
200
|
-
```json
|
|
201
|
-
promotions_create({
|
|
202
|
-
"name": "20% Off with Code",
|
|
203
|
-
"status": "active",
|
|
204
|
-
"requiresCoupon": true,
|
|
205
|
-
"conditions": { "conditions": [], "combinator": "AND" },
|
|
206
|
-
"itemsDiscountComputation": {
|
|
207
|
-
"script": "const items = $('cart').items;\nreturn items.map(item => ({ id: item.id, amount: item.price * 0.20 }));",
|
|
208
|
-
"language": "javascript"
|
|
209
|
-
}
|
|
210
|
-
})
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
**Step 2:** Create the coupon:
|
|
214
|
-
|
|
215
|
-
```json
|
|
216
|
-
promotions_create_coupon({
|
|
217
|
-
"promotionId": "promo_xxx",
|
|
218
|
-
"code": "SAVE20",
|
|
219
|
-
"maxUses": 1000
|
|
220
|
-
})
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
**Step 3:** Test with coupon:
|
|
224
|
-
|
|
225
|
-
```json
|
|
226
|
-
promotions_evaluate_cart({
|
|
227
|
-
"cart": { "items": [{ "id": "item-1", "price": 50.00, "quantity": 1 }] },
|
|
228
|
-
"couponCodes": ["SAVE20"]
|
|
229
|
-
})
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
---
|
|
233
|
-
|
|
234
|
-
### Recipe 6: Referral Program (Refer a friend, both get 500 points)
|
|
235
|
-
|
|
236
|
-
**Domains:** Referral + Rules + Wallet
|
|
237
|
-
|
|
238
|
-
**Step 1:** Check referral config:
|
|
239
|
-
|
|
240
|
-
```json
|
|
241
|
-
referral_get_config()
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
**Step 2:** Create referral code for a user:
|
|
245
|
-
|
|
246
|
-
```json
|
|
247
|
-
referral_create_code({ "referrerId": "user-123" })
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
**Step 3:** Set up a rule to reward on completed referral:
|
|
251
|
-
|
|
252
|
-
```json
|
|
253
|
-
project_add_rule({
|
|
254
|
-
"name": "Referral Reward",
|
|
255
|
-
"conditions": {
|
|
256
|
-
"conditions": [
|
|
257
|
-
{ "leftValue": "{{ event.type }}", "operator": "equals", "rightValue": "referral.completed", "rightType": "string" }
|
|
258
|
-
],
|
|
259
|
-
"combinator": "AND"
|
|
260
|
-
},
|
|
261
|
-
"actions": [
|
|
262
|
-
{
|
|
263
|
-
"type": "reward",
|
|
264
|
-
"config": {
|
|
265
|
-
"assetType": "points",
|
|
266
|
-
"amount": 500,
|
|
267
|
-
"description": "Referral reward"
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
],
|
|
271
|
-
"resources": {
|
|
272
|
-
"event": { "type": "referral.completed" }
|
|
273
|
-
}
|
|
274
|
-
})
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
---
|
|
278
|
-
|
|
279
|
-
### Recipe 7: Stacking Promotions (Member discount + seasonal sale)
|
|
280
|
-
|
|
281
|
-
**Domains:** Promotions (stacking)
|
|
282
|
-
|
|
283
|
-
Create two stackable promotions:
|
|
284
|
-
|
|
285
|
-
```json
|
|
286
|
-
// Always-on member discount (lower priority)
|
|
287
|
-
promotions_create({
|
|
288
|
-
"name": "Member 5% Discount",
|
|
289
|
-
"status": "active",
|
|
290
|
-
"stackingMode": "stackable",
|
|
291
|
-
"priority": 1,
|
|
292
|
-
"conditions": { "conditions": [], "combinator": "AND" },
|
|
293
|
-
"itemsDiscountComputation": {
|
|
294
|
-
"script": "return $('cart').items.map(i => ({ id: i.id, amount: i.price * 0.05 }));",
|
|
295
|
-
"language": "javascript"
|
|
296
|
-
}
|
|
297
|
-
})
|
|
298
|
-
|
|
299
|
-
// Seasonal sale (higher priority, also stackable)
|
|
300
|
-
promotions_create({
|
|
301
|
-
"name": "Summer Sale 15%",
|
|
302
|
-
"status": "active",
|
|
303
|
-
"stackingMode": "stackable",
|
|
304
|
-
"priority": 10,
|
|
305
|
-
"validFrom": "2026-06-01T00:00:00Z",
|
|
306
|
-
"validTo": "2026-08-31T23:59:59Z",
|
|
307
|
-
"conditions": { "conditions": [], "combinator": "AND" },
|
|
308
|
-
"itemsDiscountComputation": {
|
|
309
|
-
"script": "return $('cart').items.map(i => ({ id: i.id, amount: i.price * 0.15 }));",
|
|
310
|
-
"language": "javascript"
|
|
311
|
-
}
|
|
312
|
-
})
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
Both apply. Use `exclusive` stacking mode if only the best should win.
|
|
316
|
-
|
|
317
|
-
---
|
|
318
|
-
|
|
319
|
-
## Tool Reference
|
|
320
|
-
|
|
321
|
-
### Promotions (`promotions_*`)
|
|
322
|
-
|
|
323
|
-
| Tool | Type | Description |
|
|
324
|
-
| -------------------------------- | ----- | -------------------------------- |
|
|
325
|
-
| `promotions_list` | read | List promotions with filters |
|
|
326
|
-
| `promotions_get_active` | read | Get all active promotions |
|
|
327
|
-
| `promotions_list_coupons` | read | List coupons for a promotion |
|
|
328
|
-
| `promotions_list_usage` | read | List usage/redemption records |
|
|
329
|
-
| `promotions_get_stats` | read | Stats for a specific promotion |
|
|
330
|
-
| `promotions_get_aggregate_stats` | read | Global promotion stats |
|
|
331
|
-
| `promotions_create` | write | Create a promotion |
|
|
332
|
-
| `promotions_update` | write | Update a promotion |
|
|
333
|
-
| `promotions_remove` | write | Delete a promotion |
|
|
334
|
-
| `promotions_duplicate` | write | Clone a promotion as draft |
|
|
335
|
-
| `promotions_create_coupon` | write | Create a coupon |
|
|
336
|
-
| `promotions_bulk_create_coupons` | write | Bulk create 1-1000 coupons |
|
|
337
|
-
| `promotions_evaluate_cart` | write | Evaluate cart against promotions |
|
|
338
|
-
|
|
339
|
-
### Wallet (`wallet_*`) — all require `userId`
|
|
340
|
-
|
|
341
|
-
| Tool | Type | Description |
|
|
342
|
-
| -------------------------- | ----- | ------------------------------- |
|
|
343
|
-
| `wallet_get_balance` | read | Get balance for all asset types |
|
|
344
|
-
| `wallet_list_transactions` | read | Transaction history |
|
|
345
|
-
| `wallet_credit` | write | Add assets (points, tokens) |
|
|
346
|
-
| `wallet_debit` | write | Spend assets (FEFO order) |
|
|
347
|
-
|
|
348
|
-
### Rules (`project_*`)
|
|
349
|
-
|
|
350
|
-
| Tool | Type | Description |
|
|
351
|
-
| --------------------------- | ----- | ----------------------------------- |
|
|
352
|
-
| `project_get_config` | read | Project configuration |
|
|
353
|
-
| `project_list_rules` | read | List loyalty rules |
|
|
354
|
-
| `project_get_rule` | read | Get a single rule |
|
|
355
|
-
| `project_list_integrations` | read | List integrations |
|
|
356
|
-
| `project_list_resources` | read | List resource definitions |
|
|
357
|
-
| `project_add_rule` | write | Create a rule |
|
|
358
|
-
| `project_update_rule` | write | Update a rule |
|
|
359
|
-
| `project_remove_rule` | write | Delete a rule |
|
|
360
|
-
| `project_test_conditions` | write | Test conditions against sample data |
|
|
361
|
-
|
|
362
|
-
### Events (`events_*`)
|
|
363
|
-
|
|
364
|
-
| Tool | Type | Description |
|
|
365
|
-
| ------------------ | ---- | ------------------------ |
|
|
366
|
-
| `events_get` | read | Get event by ID |
|
|
367
|
-
| `events_list` | read | List events with filters |
|
|
368
|
-
| `events_get_stats` | read | Event stats |
|
|
369
|
-
|
|
370
|
-
### Referral (`referral_*`)
|
|
371
|
-
|
|
372
|
-
| Tool | Type | Description |
|
|
373
|
-
| -------------------------- | ----- | ----------------------- |
|
|
374
|
-
| `referral_get_config` | read | Referral program config |
|
|
375
|
-
| `referral_list_codes` | read | List referral codes |
|
|
376
|
-
| `referral_validate_code` | read | Check if code is valid |
|
|
377
|
-
| `referral_get_stats` | read | Aggregate stats |
|
|
378
|
-
| `referral_get_leaderboard` | read | Top referrers |
|
|
379
|
-
| `referral_create_code` | write | Create a referral code |
|
|
380
|
-
| `referral_apply_code` | write | Apply code for referee |
|
|
381
|
-
| `referral_deactivate_code` | write | Deactivate a code |
|
|
382
|
-
|
|
383
|
-
### External Users (`external_users_*`)
|
|
384
|
-
|
|
385
|
-
| Tool | Type | Description |
|
|
386
|
-
| ---------------------------- | ----- | ---------------------------- |
|
|
387
|
-
| `external_users_get_user` | read | Get user by ID |
|
|
388
|
-
| `external_users_list_users` | read | List users |
|
|
389
|
-
| `external_users_lookup_user` | read | Smart lookup (ID then email) |
|
|
390
|
-
| `external_users_register` | write | Register a user |
|
|
391
|
-
| `external_users_update` | write | Update a user |
|
|
392
|
-
|
|
393
|
-
### Skill & Feedback (`skill_*`)
|
|
394
|
-
|
|
395
|
-
| Tool | Type | Description |
|
|
396
|
-
| -------------------------- | ----- | ---------------------------- |
|
|
397
|
-
| `skill_get_content` | read | Get this skill guide content |
|
|
398
|
-
| `skill_list_feedback` | read | List feedback entries |
|
|
399
|
-
| `skill_get_feedback_stats` | read | Feedback analytics |
|
|
400
|
-
| `skill_update_content` | write | Update skill guide |
|
|
401
|
-
| `skill_submit_feedback` | write | Report tool usage outcome |
|
|
402
|
-
|
|
403
|
-
---
|
|
404
|
-
|
|
405
|
-
## Discount Script Reference
|
|
406
|
-
|
|
407
|
-
**Available context:**
|
|
408
|
-
|
|
409
|
-
- `$('cart')` — full cart object. `$('cart').items` = items array
|
|
410
|
-
- `$('cart').items[n]` — `{ id, price, quantity, category, tags, ...custom }`
|
|
411
|
-
- `$('cart').total` — cart total
|
|
412
|
-
- `$('user')` — current user (if userId provided)
|
|
413
|
-
- `$('couponCodes')` — applied coupon codes array
|
|
414
|
-
|
|
415
|
-
**Return format:** `Array<{ id: string, amount: number }>` where `id` = item ID, `amount` = discount
|
|
416
|
-
|
|
417
|
-
**Common patterns:**
|
|
418
|
-
|
|
419
|
-
```javascript
|
|
420
|
-
// Percentage off all items
|
|
421
|
-
return $("cart").items.map((i) => ({ id: i.id, amount: i.price * RATE }));
|
|
422
|
-
|
|
423
|
-
// Fixed amount distributed proportionally
|
|
424
|
-
const total = $("cart").items.reduce((s, i) => s + i.price, 0);
|
|
425
|
-
return $("cart").items.map((i) => ({
|
|
426
|
-
id: i.id,
|
|
427
|
-
amount: (i.price / total) * FIXED_AMOUNT,
|
|
428
|
-
}));
|
|
429
|
-
|
|
430
|
-
// Category-specific discount
|
|
431
|
-
return $("cart")
|
|
432
|
-
.items.filter((i) => i.category === "TARGET")
|
|
433
|
-
.map((i) => ({ id: i.id, amount: i.price * RATE }));
|
|
434
|
-
|
|
435
|
-
// Cheapest item free
|
|
436
|
-
const sorted = [...$("cart").items].sort((a, b) => a.price - b.price);
|
|
437
|
-
return [{ id: sorted[0].id, amount: sorted[0].price }];
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
---
|
|
441
|
-
|
|
442
|
-
## Condition Operators
|
|
443
|
-
|
|
444
|
-
`equals`, `notEquals`, `gt`, `gte`, `lt`, `lte`, `contains`, `notContains`, `startsWith`, `endsWith`, `exists`, `notExists`, `regex`, `before`, `after`, `isEmpty`, `isNotEmpty`, `hasKey`
|
|
445
|
-
|
|
446
|
-
**Condition fields for promotions:** `cart.total`, `cart.itemCount`, `cart.uniqueItemCount`, `cart.items[0].price`, `cart.items[0].category`
|
|
447
|
-
|
|
448
|
-
**Condition expressions for rules:** `{{ event.type }}`, `{{ event.payload.amount }}`, `{{ user.email }}`, `{{ history.amount }}`
|
|
449
|
-
|
|
450
|
-
---
|
|
451
|
-
|
|
452
|
-
## Common Mistakes
|
|
453
|
-
|
|
454
|
-
1. **Script returns empty array** — Usually a field name mismatch. Check the actual cart item fields (e.g., `category` vs `type`). Always test with `promotions_evaluate_cart` first.
|
|
455
|
-
|
|
456
|
-
2. **Promotion not applying** — Check `notApplied` array in evaluate response. It tells you exactly which conditions failed and why.
|
|
457
|
-
|
|
458
|
-
3. **Rule not firing** — Ensure the event type in conditions matches what's being ingested. Use `events_list` to check actual event types.
|
|
459
|
-
|
|
460
|
-
4. **Forgot to activate** — Promotions created in `draft` won't apply. Update status to `active` after testing.
|
|
461
|
-
|
|
462
|
-
5. **Coupon not working** — Promotion must have `requiresCoupon: true` AND the coupon must be passed in `couponCodes` array during evaluation.
|
|
463
|
-
|
|
464
|
-
6. **Stacking conflicts** — `exclusive` promotions block everything else. Use `stackable` for promotions that should combine. Use `exclusive_group` for "best of group" behavior.
|
|
465
|
-
|
|
466
|
-
7. **Missing event resource in rules** — Rules require `resources: { event: { type: "..." } }`. Without it, the rule won't match any events.
|
|
467
|
-
|
|
468
|
-
---
|
|
469
|
-
|
|
470
|
-
## Feedback
|
|
471
|
-
|
|
472
|
-
After building a loyalty program, use `skill_submit_feedback` to report the outcome. This helps improve this guide over time.
|
|
473
|
-
|
|
474
|
-
```json
|
|
475
|
-
skill_submit_feedback({
|
|
476
|
-
"toolName": "promotions.create",
|
|
477
|
-
"action": "created",
|
|
478
|
-
"context": "Building a BOGO promotion for shoes",
|
|
479
|
-
"outcome": "success",
|
|
480
|
-
"details": "Worked on first try with category filter"
|
|
481
|
-
})
|
|
482
|
-
```
|