@storelayer/mcp-server 0.3.0 → 0.5.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 +183 -57
- 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,91 @@ 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
|
-
var
|
|
13661
|
+
var SKILLS_REPO = "storelayer/skills";
|
|
13662
|
+
var SKILLS_BASE_PATH = "skills/storelayer";
|
|
13663
|
+
var SKILL_REFERENCES = [
|
|
13664
|
+
{
|
|
13665
|
+
name: "architecture",
|
|
13666
|
+
description: "Platform architecture and patterns",
|
|
13667
|
+
path: "references/architecture.md"
|
|
13668
|
+
},
|
|
13669
|
+
{
|
|
13670
|
+
name: "wallet",
|
|
13671
|
+
description: "Wallet/ledger system reference",
|
|
13672
|
+
path: "references/wallet.md"
|
|
13673
|
+
},
|
|
13674
|
+
{
|
|
13675
|
+
name: "promotions",
|
|
13676
|
+
description: "Promotion engine reference",
|
|
13677
|
+
path: "references/promotions.md"
|
|
13678
|
+
},
|
|
13679
|
+
{
|
|
13680
|
+
name: "referral",
|
|
13681
|
+
description: "Referral system reference",
|
|
13682
|
+
path: "references/referral.md"
|
|
13683
|
+
},
|
|
13684
|
+
{
|
|
13685
|
+
name: "events",
|
|
13686
|
+
description: "Events and rules reference",
|
|
13687
|
+
path: "references/events.md"
|
|
13688
|
+
},
|
|
13689
|
+
{
|
|
13690
|
+
name: "external-users",
|
|
13691
|
+
description: "Customer management reference",
|
|
13692
|
+
path: "references/external-users.md"
|
|
13693
|
+
},
|
|
13694
|
+
{
|
|
13695
|
+
name: "discount-scripts",
|
|
13696
|
+
description: "Discount script reference",
|
|
13697
|
+
path: "references/discount-scripts.md"
|
|
13698
|
+
},
|
|
13699
|
+
{
|
|
13700
|
+
name: "conditions",
|
|
13701
|
+
description: "Condition engine reference",
|
|
13702
|
+
path: "references/conditions.md"
|
|
13703
|
+
},
|
|
13704
|
+
{
|
|
13705
|
+
name: "mcp-tools",
|
|
13706
|
+
description: "All MCP tools reference",
|
|
13707
|
+
path: "references/mcp-tools.md"
|
|
13708
|
+
}
|
|
13709
|
+
];
|
|
13710
|
+
var SKILL_AGENTS = [
|
|
13711
|
+
{
|
|
13712
|
+
name: "loyalty-builder",
|
|
13713
|
+
description: "Design and build complete loyalty programs",
|
|
13714
|
+
path: "agents/loyalty-builder.md"
|
|
13715
|
+
},
|
|
13716
|
+
{
|
|
13717
|
+
name: "promo-engineer",
|
|
13718
|
+
description: "Build promotions, discount scripts, and coupon campaigns",
|
|
13719
|
+
path: "agents/promo-engineer.md"
|
|
13720
|
+
},
|
|
13721
|
+
{
|
|
13722
|
+
name: "integration-dev",
|
|
13723
|
+
description: "Set up event ingestion and external integrations",
|
|
13724
|
+
path: "agents/integration-dev.md"
|
|
13725
|
+
}
|
|
13726
|
+
];
|
|
13727
|
+
var SKILL_TOOLS = [
|
|
13728
|
+
{
|
|
13729
|
+
name: "setup-project",
|
|
13730
|
+
description: "Step-by-step new project setup",
|
|
13731
|
+
path: "tools/setup-project.md"
|
|
13732
|
+
},
|
|
13733
|
+
{
|
|
13734
|
+
name: "test-promotion",
|
|
13735
|
+
description: "Test promotions before activation",
|
|
13736
|
+
path: "tools/test-promotion.md"
|
|
13737
|
+
},
|
|
13738
|
+
{
|
|
13739
|
+
name: "debug-rules",
|
|
13740
|
+
description: "Debug rules and promotions",
|
|
13741
|
+
path: "tools/debug-rules.md"
|
|
13742
|
+
}
|
|
13743
|
+
];
|
|
13674
13744
|
function apiHeaders() {
|
|
13675
13745
|
return {
|
|
13676
13746
|
Authorization: `Bearer ${API_KEY}`,
|
|
@@ -13678,8 +13748,8 @@ function apiHeaders() {
|
|
|
13678
13748
|
};
|
|
13679
13749
|
}
|
|
13680
13750
|
async function fetchToolManifest() {
|
|
13681
|
-
const url = `${API_URL}/
|
|
13682
|
-
const res = await fetch(url
|
|
13751
|
+
const url = `${API_URL}/public/tools`;
|
|
13752
|
+
const res = await fetch(url);
|
|
13683
13753
|
if (!res.ok) {
|
|
13684
13754
|
const body = await res.text().catch(() => "");
|
|
13685
13755
|
throw new Error(`Failed to fetch tool manifest (${res.status}): ${body}`);
|
|
@@ -13687,26 +13757,8 @@ async function fetchToolManifest() {
|
|
|
13687
13757
|
const json = await res.json();
|
|
13688
13758
|
return json.data.tools;
|
|
13689
13759
|
}
|
|
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
13760
|
async function executeTool(toolName, params, userId) {
|
|
13709
|
-
const url = `${API_URL}/
|
|
13761
|
+
const url = `${API_URL}/public/tools/${toolName}/execute`;
|
|
13710
13762
|
const body = { params };
|
|
13711
13763
|
if (userId)
|
|
13712
13764
|
body.userId = userId;
|
|
@@ -13724,6 +13776,31 @@ async function executeTool(toolName, params, userId) {
|
|
|
13724
13776
|
throw new Error(json.error || "Tool execution failed");
|
|
13725
13777
|
return json.data;
|
|
13726
13778
|
}
|
|
13779
|
+
var contentCache = new Map;
|
|
13780
|
+
var CACHE_TTL_MS = 15 * 60 * 1000;
|
|
13781
|
+
async function fetchSkillFile(relativePath) {
|
|
13782
|
+
const cacheKey = relativePath;
|
|
13783
|
+
const cached2 = contentCache.get(cacheKey);
|
|
13784
|
+
if (cached2 && Date.now() - cached2.fetchedAt < CACHE_TTL_MS) {
|
|
13785
|
+
return cached2.content;
|
|
13786
|
+
}
|
|
13787
|
+
const url = `https://raw.githubusercontent.com/${SKILLS_REPO}/main/${SKILLS_BASE_PATH}/${relativePath}`;
|
|
13788
|
+
const res = await fetch(url);
|
|
13789
|
+
if (!res.ok) {
|
|
13790
|
+
throw new Error(`Failed to fetch ${relativePath} from GitHub (${res.status})`);
|
|
13791
|
+
}
|
|
13792
|
+
const content = await res.text();
|
|
13793
|
+
contentCache.set(cacheKey, { content, fetchedAt: Date.now() });
|
|
13794
|
+
return content;
|
|
13795
|
+
}
|
|
13796
|
+
async function fetchSkillGuide() {
|
|
13797
|
+
try {
|
|
13798
|
+
return await fetchSkillFile("SKILL.md");
|
|
13799
|
+
} catch (error2) {
|
|
13800
|
+
console.error("Failed to fetch SKILL.md from GitHub:", error2);
|
|
13801
|
+
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).";
|
|
13802
|
+
}
|
|
13803
|
+
}
|
|
13727
13804
|
function buildInputSchema(tool) {
|
|
13728
13805
|
const schema = structuredClone(tool.parameters);
|
|
13729
13806
|
if (!schema.type)
|
|
@@ -13752,17 +13829,13 @@ async function main() {
|
|
|
13752
13829
|
console.error("Error: STORE_LAYER_API_KEY environment variable is required");
|
|
13753
13830
|
process.exit(1);
|
|
13754
13831
|
}
|
|
13755
|
-
if (!PROJECT_ID) {
|
|
13756
|
-
console.error("Error: STORE_LAYER_PROJECT_ID environment variable is required");
|
|
13757
|
-
process.exit(1);
|
|
13758
|
-
}
|
|
13759
13832
|
console.error(`Connecting to Store Layer API at ${API_URL}...`);
|
|
13760
|
-
const [manifest,
|
|
13833
|
+
const [manifest, skillGuide] = await Promise.all([
|
|
13761
13834
|
fetchToolManifest(),
|
|
13762
|
-
|
|
13835
|
+
fetchSkillGuide()
|
|
13763
13836
|
]);
|
|
13764
13837
|
console.error(`Loaded ${manifest.length} tools from Store Layer`);
|
|
13765
|
-
console.error(`Skill guide
|
|
13838
|
+
console.error(`Skill guide loaded from GitHub (${SKILLS_REPO})`);
|
|
13766
13839
|
const mcpToRegistry = new Map;
|
|
13767
13840
|
const mcpToManifest = new Map;
|
|
13768
13841
|
for (const tool of manifest) {
|
|
@@ -13770,7 +13843,12 @@ async function main() {
|
|
|
13770
13843
|
mcpToRegistry.set(mcpName, tool.name);
|
|
13771
13844
|
mcpToManifest.set(mcpName, tool);
|
|
13772
13845
|
}
|
|
13773
|
-
const
|
|
13846
|
+
const allSkillFiles = [
|
|
13847
|
+
...SKILL_REFERENCES.map((f) => ({ ...f, category: "reference" })),
|
|
13848
|
+
...SKILL_AGENTS.map((f) => ({ ...f, category: "agent" })),
|
|
13849
|
+
...SKILL_TOOLS.map((f) => ({ ...f, category: "tool" }))
|
|
13850
|
+
];
|
|
13851
|
+
const server = new Server({ name: "store-layer", version: "0.5.0" }, { capabilities: { tools: {}, prompts: {}, resources: {} } });
|
|
13774
13852
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
13775
13853
|
tools: manifest.map((tool) => ({
|
|
13776
13854
|
name: toMcpName(tool.name),
|
|
@@ -13821,26 +13899,74 @@ async function main() {
|
|
|
13821
13899
|
prompts: [
|
|
13822
13900
|
{
|
|
13823
13901
|
name: "store-layer-guide",
|
|
13824
|
-
description:
|
|
13825
|
-
}
|
|
13902
|
+
description: "Comprehensive guide for using Storelayer tools — covers all domains (promotions, wallets, users, events, referrals, rules), with recipes and best practices."
|
|
13903
|
+
},
|
|
13904
|
+
...SKILL_AGENTS.map((agent) => ({
|
|
13905
|
+
name: agent.name,
|
|
13906
|
+
description: agent.description
|
|
13907
|
+
}))
|
|
13826
13908
|
]
|
|
13827
13909
|
}));
|
|
13828
13910
|
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
13829
|
-
|
|
13830
|
-
|
|
13911
|
+
const promptName = request.params.name;
|
|
13912
|
+
if (promptName === "store-layer-guide") {
|
|
13913
|
+
return {
|
|
13914
|
+
description: "Storelayer MCP skill guide",
|
|
13915
|
+
messages: [
|
|
13916
|
+
{
|
|
13917
|
+
role: "user",
|
|
13918
|
+
content: { type: "text", text: skillGuide }
|
|
13919
|
+
}
|
|
13920
|
+
]
|
|
13921
|
+
};
|
|
13831
13922
|
}
|
|
13832
|
-
|
|
13833
|
-
|
|
13834
|
-
|
|
13835
|
-
|
|
13836
|
-
|
|
13837
|
-
|
|
13838
|
-
|
|
13839
|
-
|
|
13923
|
+
const agent = SKILL_AGENTS.find((a) => a.name === promptName);
|
|
13924
|
+
if (agent) {
|
|
13925
|
+
try {
|
|
13926
|
+
const content = await fetchSkillFile(agent.path);
|
|
13927
|
+
return {
|
|
13928
|
+
description: agent.description,
|
|
13929
|
+
messages: [
|
|
13930
|
+
{
|
|
13931
|
+
role: "user",
|
|
13932
|
+
content: { type: "text", text: content }
|
|
13933
|
+
}
|
|
13934
|
+
]
|
|
13935
|
+
};
|
|
13936
|
+
} catch (error2) {
|
|
13937
|
+
throw new Error(`Failed to fetch agent ${agent.name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
13938
|
+
}
|
|
13939
|
+
}
|
|
13940
|
+
throw new Error(`Unknown prompt: ${promptName}`);
|
|
13941
|
+
});
|
|
13942
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
13943
|
+
resources: allSkillFiles.map((file) => ({
|
|
13944
|
+
uri: `storelayer://${file.category}/${file.name}`,
|
|
13945
|
+
name: file.name,
|
|
13946
|
+
description: file.description,
|
|
13947
|
+
mimeType: "text/markdown"
|
|
13948
|
+
}))
|
|
13949
|
+
}));
|
|
13950
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
13951
|
+
const uri = request.params.uri;
|
|
13952
|
+
const file = allSkillFiles.find((f) => `storelayer://${f.category}/${f.name}` === uri);
|
|
13953
|
+
if (!file) {
|
|
13954
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
13955
|
+
}
|
|
13956
|
+
try {
|
|
13957
|
+
const content = await fetchSkillFile(file.path);
|
|
13958
|
+
return {
|
|
13959
|
+
contents: [
|
|
13960
|
+
{
|
|
13961
|
+
uri,
|
|
13962
|
+
mimeType: "text/markdown",
|
|
13963
|
+
text: content
|
|
13840
13964
|
}
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
}
|
|
13965
|
+
]
|
|
13966
|
+
};
|
|
13967
|
+
} catch (error2) {
|
|
13968
|
+
throw new Error(`Failed to fetch ${file.path}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
13969
|
+
}
|
|
13844
13970
|
});
|
|
13845
13971
|
const transport = new StdioServerTransport;
|
|
13846
13972
|
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.5.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
|
-
```
|