@open-loyalty/mcp-server 1.3.7 → 1.5.3

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 (100) hide show
  1. package/dist/config.d.ts +4 -3
  2. package/dist/config.js +9 -7
  3. package/dist/instructions.d.ts +1 -1
  4. package/dist/instructions.js +26 -8
  5. package/dist/tools/achievement/handlers.js +60 -1
  6. package/dist/tools/achievement/index.d.ts +8 -8
  7. package/dist/tools/achievement/index.js +10 -11
  8. package/dist/tools/achievement/schemas.d.ts +8 -8
  9. package/dist/tools/achievement/schemas.js +36 -32
  10. package/dist/tools/analytics.js +9 -9
  11. package/dist/tools/badge.js +4 -4
  12. package/dist/tools/campaign/handlers.js +35 -1
  13. package/dist/tools/campaign/index.d.ts +97 -34
  14. package/dist/tools/campaign/index.js +14 -12
  15. package/dist/tools/campaign/schemas.d.ts +96 -33
  16. package/dist/tools/campaign/schemas.js +89 -55
  17. package/dist/tools/custom-event.js +10 -9
  18. package/dist/tools/export.js +4 -4
  19. package/dist/tools/import.d.ts +1 -1
  20. package/dist/tools/import.js +11 -6
  21. package/dist/tools/index.js +4 -4
  22. package/dist/tools/member/handlers.js +48 -1
  23. package/dist/tools/member/index.d.ts +1 -1
  24. package/dist/tools/member/index.js +3 -1
  25. package/dist/tools/member/schemas.js +12 -12
  26. package/dist/tools/points/handlers.d.ts +75 -0
  27. package/dist/tools/{points.js → points/handlers.js} +21 -112
  28. package/dist/tools/points/index.d.ts +84 -0
  29. package/dist/tools/points/index.js +63 -0
  30. package/dist/tools/points/schemas.d.ts +45 -0
  31. package/dist/tools/points/schemas.js +47 -0
  32. package/dist/tools/referral/schemas.js +3 -3
  33. package/dist/tools/reward/handlers.d.ts +2 -0
  34. package/dist/tools/reward/handlers.js +79 -6
  35. package/dist/tools/reward/index.d.ts +2 -0
  36. package/dist/tools/reward/index.js +10 -10
  37. package/dist/tools/reward/schemas.d.ts +2 -0
  38. package/dist/tools/reward/schemas.js +42 -23
  39. package/dist/tools/segment/index.js +7 -5
  40. package/dist/tools/segment/schemas.js +11 -11
  41. package/dist/tools/tierset/handlers.d.ts +26 -0
  42. package/dist/tools/tierset/handlers.js +196 -0
  43. package/dist/tools/tierset/index.d.ts +124 -0
  44. package/dist/tools/tierset/index.js +88 -0
  45. package/dist/tools/tierset/schemas.d.ts +127 -0
  46. package/dist/tools/tierset/schemas.js +62 -0
  47. package/dist/tools/transaction/handlers.d.ts +89 -0
  48. package/dist/tools/transaction/handlers.js +159 -0
  49. package/dist/tools/transaction/index.d.ts +153 -0
  50. package/dist/tools/transaction/index.js +54 -0
  51. package/dist/tools/transaction/schemas.d.ts +126 -0
  52. package/dist/tools/transaction/schemas.js +60 -0
  53. package/dist/tools/wallet-type/handlers.d.ts +63 -0
  54. package/dist/tools/{wallet-type.js → wallet-type/handlers.js} +15 -78
  55. package/dist/tools/{wallet-type.d.ts → wallet-type/index.d.ts} +3 -64
  56. package/dist/tools/wallet-type/index.js +65 -0
  57. package/dist/tools/wallet-type/schemas.d.ts +1 -0
  58. package/dist/tools/wallet-type/schemas.js +1 -0
  59. package/dist/tools/webhook.js +6 -6
  60. package/dist/types/schemas/achievement.d.ts +48 -48
  61. package/dist/types/schemas/admin.d.ts +10 -10
  62. package/dist/types/schemas/campaign.d.ts +12 -12
  63. package/dist/types/schemas/common.js +1 -1
  64. package/dist/types/schemas/member.d.ts +18 -18
  65. package/dist/types/schemas/role.d.ts +4 -4
  66. package/dist/types/schemas/tierset.js +2 -1
  67. package/dist/types/schemas/transaction.d.ts +12 -12
  68. package/dist/types/schemas/wallet-type.js +12 -10
  69. package/dist/types/schemas/webhook.d.ts +6 -6
  70. package/dist/utils/errors.js +40 -0
  71. package/package.json +3 -2
  72. package/dist/prompts/fan-engagement-setup.d.ts +0 -107
  73. package/dist/prompts/fan-engagement-setup.js +0 -492
  74. package/dist/tools/achievement.d.ts +0 -1017
  75. package/dist/tools/achievement.js +0 -354
  76. package/dist/tools/campaign.d.ts +0 -1800
  77. package/dist/tools/campaign.js +0 -737
  78. package/dist/tools/member.d.ts +0 -366
  79. package/dist/tools/member.js +0 -352
  80. package/dist/tools/points.d.ts +0 -201
  81. package/dist/tools/reward.d.ts +0 -279
  82. package/dist/tools/reward.js +0 -361
  83. package/dist/tools/segment.d.ts +0 -816
  84. package/dist/tools/segment.js +0 -333
  85. package/dist/tools/tierset.d.ts +0 -273
  86. package/dist/tools/tierset.js +0 -289
  87. package/dist/tools/transaction.d.ts +0 -365
  88. package/dist/tools/transaction.js +0 -259
  89. package/dist/workflows/app-login-streak.d.ts +0 -39
  90. package/dist/workflows/app-login-streak.js +0 -298
  91. package/dist/workflows/early-arrival.d.ts +0 -33
  92. package/dist/workflows/early-arrival.js +0 -148
  93. package/dist/workflows/index.d.ts +0 -101
  94. package/dist/workflows/index.js +0 -208
  95. package/dist/workflows/match-attendance.d.ts +0 -45
  96. package/dist/workflows/match-attendance.js +0 -308
  97. package/dist/workflows/sportsbar-visit.d.ts +0 -41
  98. package/dist/workflows/sportsbar-visit.js +0 -284
  99. package/dist/workflows/vod-watching.d.ts +0 -43
  100. package/dist/workflows/vod-watching.js +0 -326
package/dist/config.d.ts CHANGED
@@ -2,15 +2,15 @@ import { z } from "zod";
2
2
  declare const ConfigSchema: z.ZodObject<{
3
3
  apiUrl: z.ZodString;
4
4
  apiToken: z.ZodString;
5
- defaultStoreCode: z.ZodString;
5
+ defaultStoreCode: z.ZodOptional<z.ZodString>;
6
6
  }, "strip", z.ZodTypeAny, {
7
7
  apiUrl: string;
8
8
  apiToken: string;
9
- defaultStoreCode: string;
9
+ defaultStoreCode?: string | undefined;
10
10
  }, {
11
11
  apiUrl: string;
12
12
  apiToken: string;
13
- defaultStoreCode: string;
13
+ defaultStoreCode?: string | undefined;
14
14
  }>;
15
15
  export type Config = z.infer<typeof ConfigSchema>;
16
16
  /**
@@ -24,6 +24,7 @@ export declare function runWithConfig<T>(override: {
24
24
  }, fn: () => T | Promise<T>): T | Promise<T>;
25
25
  /**
26
26
  * Gets the store code, falling back to default from config if not provided.
27
+ * Throws a clear error if no store code is available from either source.
27
28
  */
28
29
  export declare function getStoreCode(storeCode?: string): string;
29
30
  export declare function getConfig(): Config;
package/dist/config.js CHANGED
@@ -3,7 +3,7 @@ import { AsyncLocalStorage } from "async_hooks";
3
3
  const ConfigSchema = z.object({
4
4
  apiUrl: z.string().url(),
5
5
  apiToken: z.string().min(1),
6
- defaultStoreCode: z.string().min(1),
6
+ defaultStoreCode: z.string().min(1).optional(),
7
7
  });
8
8
  let config = null;
9
9
  // Request-scoped config storage using AsyncLocalStorage (thread-safe for concurrent requests)
@@ -22,9 +22,15 @@ export function runWithConfig(override, fn) {
22
22
  }
23
23
  /**
24
24
  * Gets the store code, falling back to default from config if not provided.
25
+ * Throws a clear error if no store code is available from either source.
25
26
  */
26
27
  export function getStoreCode(storeCode) {
27
- return storeCode || getConfig().defaultStoreCode;
28
+ const code = storeCode || getConfig().defaultStoreCode;
29
+ if (!code) {
30
+ throw new Error("No store code available. Either configure OPENLOYALTY_DEFAULT_STORE_CODE in server settings, " +
31
+ "or pass storeCode parameter. Use ol_store_list to see available stores.");
32
+ }
33
+ return code;
28
34
  }
29
35
  export function getConfig() {
30
36
  // Return request-scoped config if set (OAuth mode)
@@ -46,14 +52,10 @@ export function getConfig() {
46
52
  throw new Error("Missing required environment variable: OPENLOYALTY_API_TOKEN. " +
47
53
  "Please set this to your Open Loyalty API authentication token");
48
54
  }
49
- if (!defaultStoreCode) {
50
- throw new Error("Missing required environment variable: OPENLOYALTY_DEFAULT_STORE_CODE. " +
51
- "Please set this to your default store code (e.g., default)");
52
- }
53
55
  const result = ConfigSchema.safeParse({
54
56
  apiUrl,
55
57
  apiToken,
56
- defaultStoreCode,
58
+ defaultStoreCode: defaultStoreCode || undefined,
57
59
  });
58
60
  if (!result.success) {
59
61
  const errors = result.error.issues
@@ -2,4 +2,4 @@
2
2
  * MCP Server Instructions - provides context and guidance for AI agents
3
3
  * using the Open Loyalty MCP server.
4
4
  */
5
- export declare const SERVER_INSTRUCTIONS = "\nOpen Loyalty MCP Server - Complete Loyalty Program Management\n\n## Domain Model\n\nMember (loyalty program participant)\n \u2514\u2500\u2500 Points (wallet balance, transfers)\n \u2514\u2500\u2500 Tier (current level: Bronze, Silver, Gold)\n \u2514\u2500\u2500 Transactions (purchase history)\n \u2514\u2500\u2500 Rewards (redeemed coupons)\n \u2514\u2500\u2500 Achievements (gamification progress)\n \u2514\u2500\u2500 Badges (visual rewards from achievements)\n\nTierSet (loyalty program structure)\n \u2514\u2500\u2500 Conditions (criteria: activeUnits, totalSpending)\n \u2514\u2500\u2500 Tiers (levels with thresholds)\n\nWalletType (points currency configuration)\n\nReward (redeemable items)\n \u2514\u2500\u2500 Categories (reward groupings)\n\nCampaign (automated points/rewards rules)\n \u2514\u2500\u2500 Type (earning, spending, custom, instant_reward)\n \u2514\u2500\u2500 Trigger (transaction, custom_event, points_transfer)\n \u2514\u2500\u2500 Rules (SKU, category, transaction amount conditions)\n \u2514\u2500\u2500 Effects (give_points, multiply_points, percentage_discount)\n \u2514\u2500\u2500 Targeting (segments, tiers for audience)\n\nSegment (member audience grouping)\n \u2514\u2500\u2500 Parts (groups of criteria - OR logic between parts)\n \u2514\u2500\u2500 Criteria (conditions - AND logic within part)\n Types: transaction_count (working; requires min+max). Other types may be rejected by the API.\n\nAchievement (gamification goals)\n \u2514\u2500\u2500 Trigger (transaction, custom_event, points_transfer, reward_redemption, referral, achievement, tier_change, profile_update)\n \u2514\u2500\u2500 CompleteRule (periodGoal, period type, unique counting)\n \u2514\u2500\u2500 Badge (visual reward when completed)\n\nBadge (visual recognition)\n \u2514\u2500\u2500 Linked to achievements via badgeTypeId\n \u2514\u2500\u2500 Auto-created when referenced by achievement\n\n## Key Workflows\n\n### 1. Create 3-tier Loyalty Program:\nRECOMMENDED WORKFLOW:\n1. wallet_type_list \u2192 Get wallet code (usually \"default\")\n2. tierset_list \u2192 Check if tier set already exists\n3. IF EXISTS: tierset_get \u2192 Get conditionId from existing tier set\n ELSE: tierset_create \u2192 Create new, then tierset_get to get conditionId\n4. tierset_update_tiers \u2192 Define tier thresholds using conditionId\n5. tierset_get_tiers \u2192 Verify tiers created correctly\n\nGOTCHA - Valid tier condition attributes:\n- activeUnits (current spendable balance)\n- totalEarnedUnits (lifetime points - NOT 'earnedUnits'!)\n- totalSpending (lifetime spend amount)\n- monthsSinceJoiningProgram (tenure in months)\n- cumulatedEarnedUnits (similar to totalEarnedUnits)\n\nCOMMON MISTAKE: Using 'earnedUnits' will fail - use 'totalEarnedUnits' for lifetime points.\n\n### 2. Register and Reward Member:\nmember_create \u2192 points_add (welcome bonus) \u2192 member_get (verify)\n\n### 3. Record Purchase and Earn Points:\ntransaction_create (with customerData) \u2192 triggers campaigns \u2192 points auto-added\n\n### 4. Redeem Reward:\nreward_list \u2192 reward_buy (deducts points) \u2192 reward_redeem (mark coupon used)\n\n### 5. Assign Unmatched Transaction:\ntransaction_list (matched=false) \u2192 transaction_assign_member \u2192 points earned\n\n### 6. Check Member Tier Progress:\nmember_get_tier_progress \u2192 shows current tier, next tier, progress %\n\n### 7. Double Points for VIP Members:\nsegment_create (with transaction_count criteria) \u2192 campaign_create (targeting that segment with give_points effect and pointsRule: \"transaction.grossValue * 2\")\n\n### 8. Purchase Achievement with Badge:\nbadge_list (find or note badgeTypeId) \u2192 achievement_create (type+trigger+aggregation+period required, badgeTypeId)\n\n### 9. Create Targeted Promotion:\nsegment_create (define audience) \u2192 campaign_create (type: earning, target: segment, segmentIds: [segmentId])\n\n### 10. Track Member Gamification:\nachievement_list_member_achievements \u2192 badge_get_member_badges \u2192 achievement_get_member_progress\n\n## Discovery Paths:\n- Members: member_list \u2192 member_get \u2192 points_get_balance\n- Tiers: tierset_list \u2192 tierset_get \u2192 tierset_get_tiers\n- Rewards: reward_category_list \u2192 reward_list \u2192 reward_get\n- Transactions: transaction_list \u2192 transaction_get\n- Campaigns: campaign_list \u2192 campaign_get \u2192 campaign_simulate\n- Segments: segment_list \u2192 segment_get \u2192 segment_get_members \u2192 campaign_create (audience targeting)\n- Achievements: achievement_list \u2192 badge_list \u2192 achievement_create (with badge)\n- Member Gamification: achievement_list_member_achievements \u2192 badge_get_member_badges\n\n## Important Patterns:\n- TIER SETUP: conditionId REQUIRED for tier thresholds\n 1. tierset_create returns conditions but you need the id from tierset_get\n 2. The conditionId is in tierset_get response: conditions[].id (this IS the conditionId)\n 3. Use this conditionId in tierset_update_tiers for each tier's threshold\n- TIER ATTRIBUTES: Use 'totalEarnedUnits' (NOT 'earnedUnits') for lifetime points\n- walletType uses CODE (e.g., 'default'), not UUID\n- Points operations: check balance with points_get_balance before spending\n- Reward flow: reward_buy returns couponCode, use reward_redeem to mark used\n- Transactions auto-match to members via customerData (email, phone, loyaltyCardNumber)\n\n### Pagination:\n- Traditional pagination: use page + perPage params\n- Cursor pagination: provide cursor from previous response for efficient deep pagination\n- Cursor-enabled tools: member_list, transaction_list, points_get_history\n- First scroll request: cursor=\"\" (empty string)\n- Subsequent requests: use cursor value from previous response\n- When cursor provided, page param is ignored\n\n### Campaign Patterns:\n- Campaign types: direct (standard), referral (referral programs)\n- Triggers: transaction (purchase), custom_event, time, achievement, etc.\n- Effects: give_points, give_reward, deduct_unit\n- Target campaigns to segments or tiers using audience.target: \"segment\" and audience.segments array\n- campaign_create requires activity.startsAt, rules[].name, and effects[].effect (use key effect, not type). pointsRule is a STRING expression.\n\n### Points Expression (pointsRule):\npointsRule is a STRING expression, not an object. Examples:\n- Fixed: \"100\"\n- Per dollar: \"transaction.grossValue * 10\"\n- Category-based: \"transaction.category('electronics').grossValue * 5\"\n- Capped: \"(transaction.grossValue * 2 >= 100) ? 100 : transaction.grossValue * 2\"\n- Rounded: \"round_up(0.1 * transaction.grossValue)\"\n\n### Segment Patterns:\n- Parts use OR logic (member matches ANY part)\n- Criteria within a part use AND logic (member must match ALL criteria in that part)\n- Common criteria: transaction_count (working; requires min+max). Other criteria may be rejected by the API.\n\n### Achievement Patterns:\n- Triggers: transaction, custom_event, points_transfer, reward_redemption, referral, achievement, tier_change, profile_update\n- periodGoal sets the target (e.g., 5 purchases, 1000 points spent)\n- period.type defines timeframe: day, week, month, year, last_day, calendarDays, calendarWeeks, calendarMonths, calendarYears\n- aggregation.type defines counting method: quantity (count events only)\n- Link to badge via badgeTypeId - badge auto-created if doesn't exist\n- uniqueAttribute for counting distinct values (e.g., unique products)\n\n### Achievement Period Types:\n- \"day\": Count all-time when consecutive=1\n- \"week\": Count per week (use consecutive=1 for all-time weekly tracking)\n- \"month\": Count per month\n- \"year\": Count per year\n- \"last_day\": Rolling window of N days (e.g., value: 7 for last 7 days)\n- \"calendarDays\": Reset daily at midnight\n- \"calendarWeeks\": Reset weekly (Monday)\n- \"calendarMonths\": Reset monthly (1st of month)\n- \"calendarYears\": Reset yearly (January 1st)\n\n### Streak Achievement Example (7-day login streak):\n{\n \"rules\": [{\n \"trigger\": \"custom_event\",\n \"event\": \"app_login\",\n \"type\": \"direct\",\n \"aggregation\": {\"type\": \"quantity\"},\n \"completeRule\": {\n \"periodGoal\": 1,\n \"period\": {\"type\": \"last_day\", \"value\": 7, \"consecutive\": 1}\n },\n \"limit\": {\n \"interval\": {\"type\": \"calendarDays\", \"value\": 1},\n \"value\": 1\n }\n }]\n}\n\n### Custom Event Workflows:\n\n#### Create Custom Event Campaign:\n1. custom_event_schema_list \u2192 Check if event type exists\n2. custom_event_schema_create (if needed) \u2192 Define event type and fields\n3. campaign_create with trigger: \"custom_event\", event: \"{event_type}\"\n4. Send events via custom_event_send to trigger campaign\n\n#### Create Achievement with Custom Event:\n1. custom_event_schema_create \u2192 Define event (e.g., \"location_visit\")\n2. achievement_create with:\n - rules[].trigger: \"custom_event\"\n - rules[].event: \"{event_type}\"\n - rules[].type: \"direct\"\n - rules[].aggregation.type: \"quantity\"\n - rules[].completeRule.periodGoal: {count}\n - rules[].completeRule.period: { type: \"day\", consecutive: 1 } (all-time)\n\n#### Create Campaign Triggered by Achievement:\n1. achievement_create \u2192 Get achievementId from response\n2. campaign_create with:\n - trigger: \"achievement\"\n - achievementId: \"{achievement_id}\"\n - rules with effects (give_points, give_reward)\n\n### Condition Operators (for campaigns/achievements):\n- is_equal, is_not_equal\n- gte, gt, lte, lt\n- is_between, is_not_between\n- is_time_between\n- is_day_of_week, is_day_of_month\n- contains, not_contains\n- starts_with, ends_with\n- one_of, not_one_of\n- expression (for custom logic)\n\n## Common Campaign Patterns (Industry-Agnostic)\n\nThese patterns work for any loyalty program - retail, QSR, aviation, fan engagement, hospitality, etc.\n\n### Pattern 1: Count Events Toward Goal (Achievements)\nUse case: Track member actions (visits, purchases, logins, check-ins) toward a milestone.\n1. custom_event_schema_create \u2192 Define your action type (e.g., \"store_visit\", \"app_login\")\n2. achievement_create with:\n - trigger: \"custom_event\"\n - event: \"{your_event_code}\"\n - aggregation: { type: \"quantity\" }\n - completeRule: { periodGoal: {count}, period: { type: \"day\", consecutive: 1 } }\n3. custom_event_send to log each action\n\n### Pattern 2: Points Per Action (Instant Rewards)\nUse case: Award instant points for each qualifying action.\n1. custom_event_schema_create (if not using transaction)\n2. campaign_create with:\n - trigger: \"custom_event\" (or \"transaction\")\n - event: \"{event_code}\" (for custom events)\n - rules: [{ effects: [{ effect: \"give_points\", pointsRule: \"50\" }] }]\n\n### Pattern 3: Conditional Rewards (Bonus Conditions)\nUse case: Award points only when a condition is met (e.g., early arrival, large purchase).\nUse ternary expressions in pointsRule instead of conditions array:\n- pointsRule: \"event.body.minutes_before >= 60 ? 25 : 0\"\n- pointsRule: \"transaction.grossValue >= 100 ? 50 : 0\"\n\n### Pattern 4: Execution Limits (Anti-Fraud)\nUse case: Limit rewards to prevent abuse (once per day, once per week).\nCampaign limits structure:\n{\n \"limits\": {\n \"executionsPerMember\": {\n \"value\": 1,\n \"interval\": { \"type\": \"calendarDays\", \"value\": 1 }\n }\n }\n}\nNote: Use \"calendarDays\" (not \"days\"), \"calendarWeeks\", \"calendarMonths\", \"calendarYears\".\nOmit interval entirely for lifetime/forever limit.\n\n### Pattern 5: Achievement-Triggered Bonus\nUse case: Award bonus points/rewards when member completes an achievement.\n1. achievement_create \u2192 Get achievementId from response\n2. campaign_create with:\n - trigger: \"achievement\"\n - achievementId: \"{achievement_id}\"\n - rules with give_points or give_reward effect\n\n### Pattern 6: Tiered Milestones\nUse case: Multiple achievement levels (bronze/silver/gold, 5/10/25 visits).\nCreate multiple achievements with increasing periodGoal values,\neach linked to a different bonus campaign with increasing rewards.\n\n### Pattern 7: Time-Limited Promotion\nUse case: Seasonal or promotional campaigns.\n{\n \"activity\": {\n \"startsAt\": \"2026-01-01 00:00+00:00\",\n \"endsAt\": \"2026-03-31 23:59+00:00\"\n }\n}\n\n### Pattern 8: Streak Tracking\nUse case: Reward consecutive daily/weekly actions (login streaks, workout streaks).\nAchievement with:\n- completeRule.period: { type: \"last_day\", value: 7 } (rolling 7-day window)\n- limit: { value: 1, interval: { type: \"calendarDays\", value: 1 } } (max 1 per day)\n\n## Phase 3: Analytics, Admin, Roles & Stores\n\n### Domain Model (Extended)\n\nAdmin (system user)\n \u2514\u2500\u2500 Roles (permission sets)\n \u2514\u2500\u2500 Permissions (resource + access level)\n \u2514\u2500\u2500 API Keys (programmatic access)\n\nStore (multi-tenant container)\n \u2514\u2500\u2500 Members, Campaigns, Rewards (isolated per store)\n\nAudit Log (compliance tracking)\n \u2514\u2500\u2500 eventType, entityType, entityId, username, timestamp\n\n### Analytics Workflows:\n\n#### 11. Get Program Overview:\nanalytics_dashboard \u2192 analytics_tiers \u2192 analytics_members\n\n#### 12. Analyze Points Economy:\nanalytics_points \u2192 analytics_units (per wallet) \u2192 points_get_histogram\n\n#### 13. Measure Campaign Performance:\nanalytics_campaigns \u2192 analytics_campaign_detail (specific campaign)\n\n#### 14. Track Transactions:\nanalytics_transactions \u2192 transaction_list (details)\n\n### Aggregation Queries (Top Spenders, Purchase Analysis):\n\nIMPORTANT: For queries like \"top 5 spenders in July 2025\", use this approach:\n\n#### 15. Find Top Spenders by Date Range:\n1. transaction_list(purchasedAtFrom, purchasedAtTo, perPage=50, cursor='') - returns customerId with each transaction\n2. Use cursor pagination to fetch ALL pages - even if there are 1000+ transactions\n3. Aggregate grossValue by customerId in your code\n4. Sort by total spent, take top N\n5. member_get for each top spender to get names/details\n\nCRITICAL - DO NOT TRY TO BE SMART OR OPTIMIZE:\n- ALWAYS iterate through ALL pages using cursor pagination - this is the ONLY correct approach\n- DO NOT skip pages or try to sample data - you will get inaccurate results\n- DO NOT use transaction_get individually - transaction_list already includes customerId\n- DO NOT try to find \"smarter\" analytics endpoints - they don't exist for per-customer aggregation\n- Large datasets (1000+ transactions) are normal - just keep paginating until cursor is empty\n- Start with cursor='' (empty string), use returned cursor for next request, repeat until done\n\nExample: Top 5 spenders July 2025\n- First request: transaction_list(purchasedAtFrom: \"2025-07-01\", purchasedAtTo: \"2025-07-31\", perPage: 50, cursor: \"\")\n- Each result includes: transactionId, grossValue, customerId, purchasedAt\n- Keep fetching with returned cursor until response has no cursor or empty transactions\n- Even if there are 1500 transactions (30 pages), iterate through ALL of them\n- Group by customerId, sum grossValue, sort descending, take 5\n\n### Admin & Security Workflows:\n\n#### 15. Create Admin with Limited Role:\nacl_get_resources \u2192 role_create \u2192 admin_create (with roleId)\n\n#### 16. Generate API Key for Integration:\nadmin_get \u2192 apikey_create \u2192 SAVE TOKEN IMMEDIATELY (shown once!)\n\n#### 17. Audit User Actions:\naudit_list (filter by username/entity) \u2192 audit_export (compliance report)\n\n### Store Multi-Tenancy:\n\n#### 18. Create New Store:\nstore_create \u2192 configure campaigns/tiers \u2192 member_create (with storeCode)\n\n### Analytics Query Patterns:\n- All analytics tools support dateFrom/dateTo ISO date filters\n- analytics_dashboard: high-level program metrics\n- analytics_units: wallet-specific metrics (requires walletTypeCode)\n- analytics_campaign_detail: detailed metrics for single campaign\n\n### Admin \u2192 Role \u2192 Permission Model:\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Admin \u2502\u2500\u2500\u2500\u2500\u25B6\u2502 Role \u2502\u2500\u2500\u2500\u2500\u25B6\u2502 Permission \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 \u2502\n \u2502 resource + access\n \u2502 (VIEW, MODIFY, etc.)\n \u25BC\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 API Key \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n### Store Multi-Tenancy:\n- Each store is isolated: members, campaigns, rewards, transactions\n- storeCode parameter routes requests to correct tenant\n- Default store used when storeCode omitted\n- IMPORTANT: Do NOT pass storeCode parameter unless the user explicitly asks to work with a different store. The configured default store should always be used unless the user requests otherwise.\n\n## Phase 4: Webhooks, Import & Export\n\n### Domain Model (Extended)\n\nWebhook Subscription (event notification)\n \u2514\u2500\u2500 eventName (event to subscribe to)\n \u2514\u2500\u2500 url (callback endpoint)\n \u2514\u2500\u2500 headers (custom HTTP headers)\n\nImport (bulk data upload)\n \u2514\u2500\u2500 type: member, groupValue, segmentMembers, unitTransferAdding, etc.\n \u2514\u2500\u2500 status: pending, processing, succeed, failed\n \u2514\u2500\u2500 items (individual row results)\n\nExport (bulk data download)\n \u2514\u2500\u2500 type: campaignCode, member, memberTier, memberSegment, rewardFulfillment\n \u2514\u2500\u2500 status: pending, done, failed, error\n \u2514\u2500\u2500 CSV file (when status='done')\n\n### Webhook Workflows:\n\n#### 19. Subscribe to Member Events for CRM Sync:\nwebhook_events \u2192 webhook_create (eventName: 'member.created', url: 'https://crm.example.com/webhook')\n\n#### 20. List and Manage Subscriptions:\nwebhook_list \u2192 webhook_get \u2192 webhook_update or webhook_delete\n\n### Import Workflows:\n\n#### 21. Bulk Import Members from CSV:\nimport_create (type: 'member', fileContent: CSV data) \u2192 import_list \u2192 import_get (check status)\n\n#### 22. Bulk Add Points to Members:\nimport_create (type: 'unitTransferAdding', fileContent: CSV) \u2192 poll import_get until complete\n\n### Export Workflows:\n\n#### 23. Export Campaign Codes:\nexport_create (type: 'campaignCode', filters: { campaignId }) \u2192 poll export_get (until status='done') \u2192 export_download\n\n#### 24. Export Member Data:\nexport_create (type: 'member') \u2192 export_get \u2192 export_download (returns CSV)\n\n### Webhook Patterns:\n- Use webhook_events to discover available event types before subscribing\n- API uses wrapper: { webhookSubscription: { eventName, url, headers? } }\n- Common events: member.created, member.updated, transaction.created, reward.purchased\n\n### Import Patterns:\n- Import is async: create returns importId, poll status with import_get\n- CSV format required - provide plain text, not base64\n- Types: member, groupValue, segmentMembers, unitTransferAdding, unitTransferSpending, transaction, campaignCode, rewardCoupon\n\n### Export Patterns:\n- Export is async: create returns exportId, poll status until 'done'\n- API body wrapper varies by type: { campaignCode: { filters... } }\n- Only call export_download when status='done'\n- Types: campaignCode, member, memberTier, memberSegment, rewardFulfillment\n";
5
+ export declare const SERVER_INSTRUCTIONS = "\nOpen Loyalty MCP Server - Complete Loyalty Program Management\n\n## Connection & Configuration\n- API URL and API Key are PRE-CONFIGURED via server settings. NEVER ask the user for these values.\n- Store code may be pre-configured. If it is, the storeCode parameter on tools is auto-filled \u2014 do NOT prompt for it.\n- If no default store code is configured, ask the user which store to use at the START of the conversation (use ol_store_list to show options), then pass storeCode on all subsequent tool calls.\n- If the user wants to switch stores mid-conversation, they will tell you explicitly \u2014 only then change the storeCode parameter.\n\n## Domain Model\n\nMember (loyalty program participant)\n \u2514\u2500\u2500 Points (wallet balance, transfers)\n \u2514\u2500\u2500 Tier (current level: Bronze, Silver, Gold)\n \u2514\u2500\u2500 Transactions (purchase history)\n \u2514\u2500\u2500 Rewards (redeemed coupons)\n \u2514\u2500\u2500 Achievements (gamification progress)\n \u2514\u2500\u2500 Badges (visual rewards from achievements)\n\nTierSet (loyalty program structure)\n \u2514\u2500\u2500 Conditions (criteria: activeUnits, totalSpending)\n \u2514\u2500\u2500 Tiers (levels with thresholds)\n\nWalletType (points currency configuration)\n\nReward (redeemable items)\n \u2514\u2500\u2500 Categories (reward groupings)\n\nCampaign (automated points/rewards rules)\n \u2514\u2500\u2500 Type (earning, spending, custom, instant_reward)\n \u2514\u2500\u2500 Trigger (transaction, custom_event, points_transfer)\n \u2514\u2500\u2500 Rules (SKU, category, transaction amount conditions)\n \u2514\u2500\u2500 Effects (give_points, multiply_points, percentage_discount)\n \u2514\u2500\u2500 Targeting (segments, tiers for audience)\n\nSegment (member audience grouping)\n \u2514\u2500\u2500 Parts (groups of criteria - OR logic between parts)\n \u2514\u2500\u2500 Criteria (conditions - AND logic within part)\n Types: transaction_count (working; requires min+max). Other types may be rejected by the API.\n\nAchievement (gamification goals)\n \u2514\u2500\u2500 Trigger (transaction, custom_event, points_transfer, reward_redemption, referral, achievement, tier_change, profile_update)\n \u2514\u2500\u2500 CompleteRule (periodGoal, period type, unique counting)\n \u2514\u2500\u2500 Badge (visual reward when completed)\n\nBadge (visual recognition)\n \u2514\u2500\u2500 Linked to achievements via badgeTypeId\n \u2514\u2500\u2500 Auto-created when referenced by achievement\n\n## Key Workflows\n\n### 1. Create Multi-tier Loyalty Program:\nCRITICAL CONCEPT: A tier set is a CONTAINER that holds ALL tiers (Bronze/Silver/Gold/etc).\n- Create exactly ONE tier set\n- Add ALL tiers to it in ONE call to tierset_update_tiers\n- DO NOT create multiple tier sets for different tiers!\n\nRECOMMENDED WORKFLOW:\n1. wallet_type_list \u2192 Get wallet code (usually \"default\")\n2. tierset_list \u2192 Check if tier set already exists\n3. IF EXISTS: tierset_get \u2192 Get conditionId from existing tier set\n ELSE: tierset_create \u2192 Create ONE tier set, then tierset_get to get conditionId\n4. tierset_update_tiers \u2192 Define ALL tier thresholds in ONE call:\n tiers: [\n { name: \"Bronze\", conditions: [{ conditionId: \"xxx\", value: 0 }] },\n { name: \"Silver\", conditions: [{ conditionId: \"xxx\", value: 500 }] },\n { name: \"Gold\", conditions: [{ conditionId: \"xxx\", value: 1000 }] },\n { name: \"Platinum\", conditions: [{ conditionId: \"xxx\", value: 2000 }] }\n ]\n5. tierset_get_tiers \u2192 Verify tiers created correctly\n\nGOTCHA - Valid tier condition attributes:\n- activeUnits (current spendable balance)\n- totalEarnedUnits (lifetime points - NOT 'earnedUnits'!)\n- totalSpending (lifetime spend amount)\n- monthsSinceJoiningProgram (tenure in months)\n\nCOMMON MISTAKE: Using 'earnedUnits' will fail - use 'totalEarnedUnits' for lifetime points.\n\n### 2. Register and Reward Member:\nmember_create \u2192 points_add (welcome bonus) \u2192 member_get (verify)\n\n### 3. Record Purchase and Earn Points:\ntransaction_create (with customerData) \u2192 triggers campaigns \u2192 points auto-added\n\n### 4. Redeem Reward:\nreward_list \u2192 reward_buy (deducts points) \u2192 reward_redeem (mark coupon used)\n\n### 5. Assign Unmatched Transaction:\ntransaction_list (matched=false) \u2192 transaction_assign_member \u2192 points earned\n\n### 6. Check Member Tier Progress:\nmember_get_tier_progress \u2192 shows current tier, next tier, progress %\n\n### 7. Double Points for VIP Members:\nsegment_create (with transaction_count criteria) \u2192 campaign_create (targeting that segment with give_points effect and pointsRule: \"transaction.grossValue * 2\")\n\n### 8. Purchase Achievement with Badge:\nbadge_list (find or note badgeTypeId) \u2192 achievement_create (type+trigger+aggregation+period required, badgeTypeId)\n\n### 9. Create Targeted Promotion:\nsegment_create (define audience) \u2192 campaign_create (type: earning, target: segment, segmentIds: [segmentId])\n\n### 10. Track Member Gamification:\nachievement_list_member_achievements \u2192 badge_get_member_badges \u2192 achievement_get_member_progress\n\n## Discovery Paths:\n- Members: member_list \u2192 member_get \u2192 points_get_balance\n- Tiers: tierset_list \u2192 tierset_get \u2192 tierset_get_tiers\n- Rewards: reward_category_list \u2192 reward_list \u2192 reward_get\n- Transactions: transaction_list \u2192 transaction_get\n- Campaigns: campaign_list \u2192 campaign_get \u2192 campaign_simulate\n- Segments: segment_list \u2192 segment_get \u2192 segment_get_members \u2192 campaign_create (audience targeting)\n- Achievements: achievement_list \u2192 badge_list \u2192 achievement_create (with badge)\n- Member Gamification: achievement_list_member_achievements \u2192 badge_get_member_badges\n\n## Important Patterns:\n- TIER SET = CONTAINER: One tier set holds ALL tiers (Bronze/Silver/Gold/etc)\n - Create ONE tier set, add ALL tiers in ONE tierset_update_tiers call\n - DO NOT create multiple tier sets for different tiers!\n- TIER SETUP: conditionId REQUIRED for tier thresholds\n 1. tierset_create returns conditions but you need the id from tierset_get\n 2. The conditionId is in tierset_get response: conditions[].id (this IS the conditionId)\n 3. Use the SAME conditionId for ALL tiers in tierset_update_tiers\n- TIER ATTRIBUTES: Use 'totalEarnedUnits' (NOT 'earnedUnits') for lifetime points\n- walletType uses CODE (e.g., 'default'), not UUID\n- Points operations: check balance with points_get_balance before spending\n- Reward flow: reward_buy returns couponCode, use reward_redeem to mark used\n- Transactions auto-match to members via customerData (email, phone, loyaltyCardNumber)\n\n### Pagination:\n- Traditional pagination: use page + perPage params\n- Cursor pagination: provide cursor from previous response for efficient deep pagination\n- Cursor-enabled tools: member_list, transaction_list, points_get_history\n- First scroll request: cursor=\"\" (empty string)\n- Subsequent requests: use cursor value from previous response\n- When cursor provided, page param is ignored\n\n### Campaign Patterns:\n- Campaign types: direct (standard), referral (referral programs)\n- Triggers: transaction (purchase), custom_event, time, achievement, etc.\n- Effects: give_points, give_reward, deduct_unit\n- Target campaigns to segments or tiers using audience.target: \"segment\" and audience.segments array\n- campaign_create requires activity.startsAt, rules[].name, and effects[].effect (use key effect, not type). pointsRule is a STRING expression.\n\n### Points Expression (pointsRule):\npointsRule is a STRING expression, not an object. Examples:\n- Fixed: \"100\"\n- Per dollar: \"transaction.grossValue * 10\"\n- Category-based: \"transaction.category('electronics').grossValue * 5\"\n- Capped: \"(transaction.grossValue * 2 >= 100) ? 100 : transaction.grossValue * 2\"\n- Rounded: \"round_up(0.1 * transaction.grossValue)\"\n\n### Segment Patterns:\n- Parts use OR logic (member matches ANY part)\n- Criteria within a part use AND logic (member must match ALL criteria in that part)\n- Common criteria: transaction_count (working; requires min+max). Other criteria may be rejected by the API.\n\n### Achievement Patterns:\n- Triggers: transaction, custom_event, points_transfer, reward_redemption, referral, achievement, tier_change, profile_update\n- periodGoal sets the target (e.g., 5 purchases, 1000 points spent)\n- period.type defines timeframe: day, week, month, year, last_day, calendarDays, calendarWeeks, calendarMonths, calendarYears\n- aggregation.type defines counting method: quantity (count events only)\n- Link to badge via badgeTypeId - badge auto-created if doesn't exist\n- uniqueAttribute for counting distinct values (e.g., unique products)\n\n### Achievement Period Types:\n- \"day\": Count all-time when consecutive=1\n- \"week\": Count per week (use consecutive=1 for all-time weekly tracking)\n- \"month\": Count per month\n- \"year\": Count per year\n- \"last_day\": Rolling window of N days (e.g., value: 7 for last 7 days)\n- \"calendarDays\": Reset daily at midnight\n- \"calendarWeeks\": Reset weekly (Monday)\n- \"calendarMonths\": Reset monthly (1st of month)\n- \"calendarYears\": Reset yearly (January 1st)\n\n### Streak Achievement Example (7-day login streak):\n{\n \"rules\": [{\n \"trigger\": \"custom_event\",\n \"event\": \"app_login\",\n \"type\": \"direct\",\n \"aggregation\": {\"type\": \"quantity\"},\n \"completeRule\": {\n \"periodGoal\": 1,\n \"period\": {\"type\": \"last_day\", \"value\": 7, \"consecutive\": 1}\n },\n \"limit\": {\n \"interval\": {\"type\": \"calendarDays\", \"value\": 1},\n \"value\": 1\n }\n }]\n}\n\n### Custom Event Workflows:\n\n#### Create Custom Event Campaign:\n1. custom_event_schema_list \u2192 Check if event type exists\n2. custom_event_schema_create (if needed) \u2192 Define event type and fields\n3. campaign_create with trigger: \"custom_event\", event: \"{event_type}\"\n4. Send events via custom_event_send to trigger campaign\n\n#### Create Achievement with Custom Event:\n1. custom_event_schema_create \u2192 Define event (e.g., \"location_visit\")\n2. achievement_create with:\n - rules[].trigger: \"custom_event\"\n - rules[].event: \"{event_type}\"\n - rules[].type: \"direct\"\n - rules[].aggregation.type: \"quantity\"\n - rules[].completeRule.periodGoal: {count}\n - rules[].completeRule.period: { type: \"day\", consecutive: 1 } (all-time)\n\n#### Create Campaign Triggered by Achievement:\n1. achievement_create \u2192 Get achievementId from response\n2. campaign_create with:\n - trigger: \"achievement\"\n - achievementId: \"{achievement_id}\"\n - rules with effects (give_points, give_reward)\n\n### Condition Operators (for campaigns/achievements):\n- is_equal, is_not_equal\n- gte, gt, lte, lt\n- is_between, is_not_between\n- is_time_between\n- is_day_of_week, is_day_of_month\n- contains, not_contains\n- starts_with, ends_with\n- one_of, not_one_of\n- expression (for custom logic)\n\n## Common Campaign Patterns (Industry-Agnostic)\n\nThese patterns work for any loyalty program - retail, QSR, aviation, fan engagement, hospitality, etc.\n\n### Pattern 1: Count Events Toward Goal (Achievements)\nUse case: Track member actions (visits, purchases, logins, check-ins) toward a milestone.\n1. custom_event_schema_create \u2192 Define your action type (e.g., \"store_visit\", \"app_login\")\n2. achievement_create with:\n - trigger: \"custom_event\"\n - event: \"{your_event_code}\"\n - aggregation: { type: \"quantity\" }\n - completeRule: { periodGoal: {count}, period: { type: \"day\", consecutive: 1 } }\n3. custom_event_send to log each action\n\n### Pattern 2: Points Per Action (Instant Rewards)\nUse case: Award instant points for each qualifying action.\n1. custom_event_schema_create (if not using transaction)\n2. campaign_create with:\n - trigger: \"custom_event\" (or \"transaction\")\n - event: \"{event_code}\" (for custom events)\n - rules: [{ effects: [{ effect: \"give_points\", pointsRule: \"50\" }] }]\n\n### Pattern 3: Conditional Rewards (Bonus Conditions)\nUse case: Award points only when a condition is met (e.g., early arrival, large purchase).\nUse ternary expressions in pointsRule instead of conditions array:\n- pointsRule: \"event.body.minutes_before >= 60 ? 25 : 0\"\n- pointsRule: \"transaction.grossValue >= 100 ? 50 : 0\"\n\n### Pattern 4: Execution Limits (Anti-Fraud)\nUse case: Limit rewards to prevent abuse (once per day, once per week).\nCampaign limits structure:\n{\n \"limits\": {\n \"executionsPerMember\": {\n \"value\": 1,\n \"interval\": { \"type\": \"calendarDays\", \"value\": 1 }\n }\n }\n}\nNote: Use \"calendarDays\" (not \"days\"), \"calendarWeeks\", \"calendarMonths\", \"calendarYears\".\nOmit interval entirely for lifetime/forever limit.\n\n### Pattern 5: Achievement-Triggered Bonus\nUse case: Award bonus points/rewards when member completes an achievement.\n1. achievement_create \u2192 Get achievementId from response\n2. campaign_create with:\n - trigger: \"achievement\"\n - achievementId: \"{achievement_id}\"\n - rules with give_points or give_reward effect\n\n### Pattern 6: Tiered Milestones\nUse case: Multiple achievement levels (bronze/silver/gold, 5/10/25 visits).\nCreate multiple achievements with increasing periodGoal values,\neach linked to a different bonus campaign with increasing rewards.\n\n### Pattern 7: Time-Limited Promotion\nUse case: Seasonal or promotional campaigns.\n{\n \"activity\": {\n \"startsAt\": \"2026-01-01 00:00+00:00\",\n \"endsAt\": \"2026-03-31 23:59+00:00\"\n }\n}\n\n### Pattern 8: Streak Tracking\nUse case: Reward consecutive daily/weekly actions (login streaks, workout streaks).\nAchievement with:\n- completeRule.period: { type: \"last_day\", value: 7 } (rolling 7-day window)\n- limit: { value: 1, interval: { type: \"calendarDays\", value: 1 } } (max 1 per day)\n\n## Phase 3: Analytics, Admin, Roles & Stores\n\n### Domain Model (Extended)\n\nAdmin (system user)\n \u2514\u2500\u2500 Roles (permission sets)\n \u2514\u2500\u2500 Permissions (resource + access level)\n \u2514\u2500\u2500 API Keys (programmatic access)\n\nStore (multi-tenant container)\n \u2514\u2500\u2500 Members, Campaigns, Rewards (isolated per store)\n\nAudit Log (compliance tracking)\n \u2514\u2500\u2500 eventType, entityType, entityId, username, timestamp\n\n### Analytics Workflows:\n\n#### 11. Get Program Overview:\nanalytics_dashboard \u2192 analytics_tiers \u2192 analytics_members\n\n#### 12. Analyze Points Economy:\nanalytics_points \u2192 analytics_units (per wallet) \u2192 points_get_histogram\n\n#### 13. Measure Campaign Performance:\nanalytics_campaigns \u2192 analytics_campaign_detail (specific campaign)\n\n#### 14. Track Transactions:\nanalytics_transactions \u2192 transaction_list (details)\n\n### Aggregation Queries (Top Spenders, Purchase Analysis):\n\nIMPORTANT: For queries like \"top 5 spenders in July 2025\", use this approach:\n\n#### 15. Find Top Spenders by Date Range:\n1. transaction_list(purchasedAtFrom, purchasedAtTo, perPage=50, cursor='') - returns customerId with each transaction\n2. Use cursor pagination to fetch ALL pages - even if there are 1000+ transactions\n3. Aggregate grossValue by customerId in your code\n4. Sort by total spent, take top N\n5. member_get for each top spender to get names/details\n\nCRITICAL - DO NOT TRY TO BE SMART OR OPTIMIZE:\n- ALWAYS iterate through ALL pages using cursor pagination - this is the ONLY correct approach\n- DO NOT skip pages or try to sample data - you will get inaccurate results\n- DO NOT use transaction_get individually - transaction_list already includes customerId\n- DO NOT try to find \"smarter\" analytics endpoints - they don't exist for per-customer aggregation\n- Large datasets (1000+ transactions) are normal - just keep paginating until cursor is empty\n- Start with cursor='' (empty string), use returned cursor for next request, repeat until done\n\nExample: Top 5 spenders July 2025\n- First request: transaction_list(purchasedAtFrom: \"2025-07-01\", purchasedAtTo: \"2025-07-31\", perPage: 50, cursor: \"\")\n- Each result includes: transactionId, grossValue, customerId, purchasedAt\n- Keep fetching with returned cursor until response has no cursor or empty transactions\n- Even if there are 1500 transactions (30 pages), iterate through ALL of them\n- Group by customerId, sum grossValue, sort descending, take 5\n\n### Admin & Security Workflows:\n\n#### 15. Create Admin with Limited Role:\nacl_get_resources \u2192 role_create \u2192 admin_create (with roleId)\n\n#### 16. Generate API Key for Integration:\nadmin_get \u2192 apikey_create \u2192 SAVE TOKEN IMMEDIATELY (shown once!)\n\n#### 17. Audit User Actions:\naudit_list (filter by username/entity) \u2192 audit_export (compliance report)\n\n### Store Multi-Tenancy:\n\n#### 18. Create New Store:\nstore_create \u2192 configure campaigns/tiers \u2192 member_create (with storeCode)\n\n### Analytics Query Patterns:\n- All analytics tools support dateFrom/dateTo ISO date filters\n- analytics_dashboard: high-level program metrics\n- analytics_units: wallet-specific metrics (requires walletTypeCode)\n- analytics_campaign_detail: detailed metrics for single campaign\n\n### Admin \u2192 Role \u2192 Permission Model:\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Admin \u2502\u2500\u2500\u2500\u2500\u25B6\u2502 Role \u2502\u2500\u2500\u2500\u2500\u25B6\u2502 Permission \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 \u2502\n \u2502 resource + access\n \u2502 (VIEW, MODIFY, etc.)\n \u25BC\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 API Key \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n### Store Multi-Tenancy:\n- Each store is isolated: members, campaigns, rewards, transactions\n- storeCode is auto-configured from server settings \u2014 NEVER ask the user for it\n- Only pass storeCode if the user explicitly requests a different store\n\n## Phase 4: Webhooks, Import & Export\n\n### Domain Model (Extended)\n\nWebhook Subscription (event notification)\n \u2514\u2500\u2500 eventName (event to subscribe to)\n \u2514\u2500\u2500 url (callback endpoint)\n \u2514\u2500\u2500 headers (custom HTTP headers)\n\nImport (bulk data upload)\n \u2514\u2500\u2500 type: member, groupValue, segmentMembers, unitTransferAdding, etc.\n \u2514\u2500\u2500 status: pending, processing, succeed, failed\n \u2514\u2500\u2500 items (individual row results)\n\nExport (bulk data download)\n \u2514\u2500\u2500 type: campaignCode, member, memberTier, memberSegment, rewardFulfillment\n \u2514\u2500\u2500 status: pending, done, failed, error\n \u2514\u2500\u2500 CSV file (when status='done')\n\n### Webhook Workflows:\n\n#### 19. Subscribe to Member Events for CRM Sync:\nwebhook_events \u2192 webhook_create (eventName: 'member.created', url: 'https://crm.example.com/webhook')\n\n#### 20. List and Manage Subscriptions:\nwebhook_list \u2192 webhook_get \u2192 webhook_update or webhook_delete\n\n### Import Workflows:\n\n#### 21. Bulk Import Members from CSV:\nimport_create (type: 'member', fileContent: CSV data) \u2192 import_list \u2192 import_get (check status)\n\n#### 22. Bulk Add Points to Members:\nimport_create (type: 'unitTransferAdding', fileContent: CSV) \u2192 poll import_get until complete\n\n### Export Workflows:\n\n#### 23. Export Campaign Codes:\nexport_create (type: 'campaignCode', filters: { campaignId }) \u2192 poll export_get (until status='done') \u2192 export_download\n\n#### 24. Export Member Data:\nexport_create (type: 'member') \u2192 export_get \u2192 export_download (returns CSV)\n\n### Webhook Patterns:\n- Use webhook_events to discover available event types before subscribing\n- API uses wrapper: { webhookSubscription: { eventName, url, headers? } }\n- Common events: member.created, member.updated, transaction.created, reward.purchased\n\n### Import Patterns:\n- Import is async: create returns importId, poll status with import_get\n- CSV format required - provide plain text, not base64\n- Types: member, groupValue, segmentMembers, unitTransferAdding, unitTransferSpending, transaction, campaignCode, rewardCoupon\n\n### Export Patterns:\n- Export is async: create returns exportId, poll status until 'done'\n- API body wrapper varies by type: { campaignCode: { filters... } }\n- Only call export_download when status='done'\n- Types: campaignCode, member, memberTier, memberSegment, rewardFulfillment\n";
@@ -5,6 +5,12 @@
5
5
  export const SERVER_INSTRUCTIONS = `
6
6
  Open Loyalty MCP Server - Complete Loyalty Program Management
7
7
 
8
+ ## Connection & Configuration
9
+ - API URL and API Key are PRE-CONFIGURED via server settings. NEVER ask the user for these values.
10
+ - Store code may be pre-configured. If it is, the storeCode parameter on tools is auto-filled — do NOT prompt for it.
11
+ - If no default store code is configured, ask the user which store to use at the START of the conversation (use ol_store_list to show options), then pass storeCode on all subsequent tool calls.
12
+ - If the user wants to switch stores mid-conversation, they will tell you explicitly — only then change the storeCode parameter.
13
+
8
14
  ## Domain Model
9
15
 
10
16
  Member (loyalty program participant)
@@ -47,13 +53,24 @@ Badge (visual recognition)
47
53
 
48
54
  ## Key Workflows
49
55
 
50
- ### 1. Create 3-tier Loyalty Program:
56
+ ### 1. Create Multi-tier Loyalty Program:
57
+ CRITICAL CONCEPT: A tier set is a CONTAINER that holds ALL tiers (Bronze/Silver/Gold/etc).
58
+ - Create exactly ONE tier set
59
+ - Add ALL tiers to it in ONE call to tierset_update_tiers
60
+ - DO NOT create multiple tier sets for different tiers!
61
+
51
62
  RECOMMENDED WORKFLOW:
52
63
  1. wallet_type_list → Get wallet code (usually "default")
53
64
  2. tierset_list → Check if tier set already exists
54
65
  3. IF EXISTS: tierset_get → Get conditionId from existing tier set
55
- ELSE: tierset_create → Create new, then tierset_get to get conditionId
56
- 4. tierset_update_tiers → Define tier thresholds using conditionId
66
+ ELSE: tierset_create → Create ONE tier set, then tierset_get to get conditionId
67
+ 4. tierset_update_tiers → Define ALL tier thresholds in ONE call:
68
+ tiers: [
69
+ { name: "Bronze", conditions: [{ conditionId: "xxx", value: 0 }] },
70
+ { name: "Silver", conditions: [{ conditionId: "xxx", value: 500 }] },
71
+ { name: "Gold", conditions: [{ conditionId: "xxx", value: 1000 }] },
72
+ { name: "Platinum", conditions: [{ conditionId: "xxx", value: 2000 }] }
73
+ ]
57
74
  5. tierset_get_tiers → Verify tiers created correctly
58
75
 
59
76
  GOTCHA - Valid tier condition attributes:
@@ -61,7 +78,6 @@ GOTCHA - Valid tier condition attributes:
61
78
  - totalEarnedUnits (lifetime points - NOT 'earnedUnits'!)
62
79
  - totalSpending (lifetime spend amount)
63
80
  - monthsSinceJoiningProgram (tenure in months)
64
- - cumulatedEarnedUnits (similar to totalEarnedUnits)
65
81
 
66
82
  COMMON MISTAKE: Using 'earnedUnits' will fail - use 'totalEarnedUnits' for lifetime points.
67
83
 
@@ -103,10 +119,13 @@ achievement_list_member_achievements → badge_get_member_badges → achievement
103
119
  - Member Gamification: achievement_list_member_achievements → badge_get_member_badges
104
120
 
105
121
  ## Important Patterns:
122
+ - TIER SET = CONTAINER: One tier set holds ALL tiers (Bronze/Silver/Gold/etc)
123
+ - Create ONE tier set, add ALL tiers in ONE tierset_update_tiers call
124
+ - DO NOT create multiple tier sets for different tiers!
106
125
  - TIER SETUP: conditionId REQUIRED for tier thresholds
107
126
  1. tierset_create returns conditions but you need the id from tierset_get
108
127
  2. The conditionId is in tierset_get response: conditions[].id (this IS the conditionId)
109
- 3. Use this conditionId in tierset_update_tiers for each tier's threshold
128
+ 3. Use the SAME conditionId for ALL tiers in tierset_update_tiers
110
129
  - TIER ATTRIBUTES: Use 'totalEarnedUnits' (NOT 'earnedUnits') for lifetime points
111
130
  - walletType uses CODE (e.g., 'default'), not UUID
112
131
  - Points operations: check balance with points_get_balance before spending
@@ -375,9 +394,8 @@ store_create → configure campaigns/tiers → member_create (with storeCode)
375
394
 
376
395
  ### Store Multi-Tenancy:
377
396
  - Each store is isolated: members, campaigns, rewards, transactions
378
- - storeCode parameter routes requests to correct tenant
379
- - Default store used when storeCode omitted
380
- - IMPORTANT: Do NOT pass storeCode parameter unless the user explicitly asks to work with a different store. The configured default store should always be used unless the user requests otherwise.
397
+ - storeCode is auto-configured from server settings — NEVER ask the user for it
398
+ - Only pass storeCode if the user explicitly requests a different store
381
399
 
382
400
  ## Phase 4: Webhooks, Import & Export
383
401
 
@@ -1,5 +1,5 @@
1
1
  import { apiGet, apiPost, apiPut, apiPatch } from "../../client/http.js";
2
- import { formatApiError } from "../../utils/errors.js";
2
+ import { formatApiError, OpenLoyaltyError } from "../../utils/errors.js";
3
3
  import { getStoreCode } from "../../config.js";
4
4
  export async function achievementList(input) {
5
5
  const storeCode = getStoreCode(input.storeCode);
@@ -56,6 +56,65 @@ export async function achievementCreate(input) {
56
56
  return { achievementId: response.achievementId };
57
57
  }
58
58
  catch (error) {
59
+ const axiosError = error;
60
+ const apiErrors = axiosError.response?.data?.errors || [];
61
+ const allMessages = [
62
+ error instanceof Error ? error.message : "",
63
+ axiosError.response?.data?.message || "",
64
+ ...apiErrors.map((e) => `${e.path || ""}: ${e.message}`),
65
+ ]
66
+ .join(" ")
67
+ .toLowerCase();
68
+ // Invalid period type (e.g., "days" instead of "day", "daily" instead of "calendarDays")
69
+ if (allMessages.includes("period") &&
70
+ (allMessages.includes("choice is invalid") ||
71
+ allMessages.includes("choice") ||
72
+ allMessages.includes("not valid"))) {
73
+ throw new OpenLoyaltyError({
74
+ code: "INVALID_PERIOD_TYPE",
75
+ message: "Invalid period type in achievement rules",
76
+ hint: "For period.type use: 'day' (NOT 'days'), 'week', 'month', 'year', 'calendarDays' (for streaks), 'calendarWeeks', 'calendarMonths', 'calendarYears'. " +
77
+ "ONE-TIME badge: period: { type: 'day', consecutive: 1 }, periodGoal: 1, NO rule limit. " +
78
+ "STREAK badge: period: { type: 'calendarDays', consecutive: 7 }, with rule limit: { value: 1, interval: { type: 'calendarDays', value: 1 } }.",
79
+ relatedTool: "ol_achievement_create",
80
+ });
81
+ }
82
+ // Invalid interval type in rule limit (e.g., "days" instead of "calendarDays")
83
+ if (allMessages.includes("interval") &&
84
+ (allMessages.includes("choice is invalid") ||
85
+ allMessages.includes("choice") ||
86
+ allMessages.includes("not valid"))) {
87
+ throw new OpenLoyaltyError({
88
+ code: "INVALID_INTERVAL_TYPE",
89
+ message: "Invalid interval type in achievement rule limit",
90
+ hint: "For limit.interval.type use: 'calendarDays' (NOT 'days'), 'calendarWeeks' (NOT 'weeks'), 'calendarMonths', 'calendarYears'. " +
91
+ "Example streak limit: { value: 1, interval: { type: 'calendarDays', value: 1 } }.",
92
+ relatedTool: "ol_achievement_create",
93
+ });
94
+ }
95
+ // Generic "choice is invalid" fallback for other enum mismatches
96
+ if (allMessages.includes("choice is invalid") || allMessages.includes("selected choice is invalid")) {
97
+ throw new OpenLoyaltyError({
98
+ code: "INVALID_ENUM_VALUE",
99
+ message: "An enum value in the achievement configuration is invalid",
100
+ hint: "Check these common enum mistakes: " +
101
+ "period.type: use 'day' NOT 'days', 'calendarDays' NOT 'days'. " +
102
+ "limit.interval.type: use 'calendarDays', 'calendarWeeks', 'calendarMonths', 'calendarYears'. " +
103
+ "trigger: use 'transaction', 'custom_event', 'points_transfer', 'reward_redemption', 'referral', 'achievement', 'tier_change', 'profile_update'. " +
104
+ "aggregation.type: use 'quantity' (only supported value).",
105
+ relatedTool: "ol_achievement_create",
106
+ });
107
+ }
108
+ // badgeTypeId not found
109
+ if (allMessages.includes("badgetype") || (allMessages.includes("badge") && allMessages.includes("not found"))) {
110
+ throw new OpenLoyaltyError({
111
+ code: "BADGE_TYPE_NOT_FOUND",
112
+ message: "The specified badgeTypeId was not found",
113
+ hint: "Use ol_badge_list() to find valid badge type IDs. " +
114
+ "If creation fails with badgeTypeId, try without it - the system will assign a default badge.",
115
+ relatedTool: "ol_achievement_create",
116
+ });
117
+ }
59
118
  throw formatApiError(error, "ol_achievement_create");
60
119
  }
61
120
  }
@@ -173,8 +173,6 @@ export declare const achievementToolDefinitions: readonly [{
173
173
  description?: string | undefined;
174
174
  }> | undefined;
175
175
  conditions?: Record<string, unknown>[] | undefined;
176
- event?: string | undefined;
177
- achievementRuleId?: string | undefined;
178
176
  limit?: {
179
177
  value?: number | undefined;
180
178
  interval?: {
@@ -182,6 +180,8 @@ export declare const achievementToolDefinitions: readonly [{
182
180
  value?: number | undefined;
183
181
  } | undefined;
184
182
  } | undefined;
183
+ event?: string | undefined;
184
+ achievementRuleId?: string | undefined;
185
185
  uniqueReferee?: boolean | undefined;
186
186
  }, {
187
187
  type: "direct" | "referral";
@@ -203,8 +203,6 @@ export declare const achievementToolDefinitions: readonly [{
203
203
  description?: string | undefined;
204
204
  }> | undefined;
205
205
  conditions?: Record<string, unknown>[] | undefined;
206
- event?: string | undefined;
207
- achievementRuleId?: string | undefined;
208
206
  limit?: {
209
207
  value?: number | undefined;
210
208
  interval?: {
@@ -212,6 +210,8 @@ export declare const achievementToolDefinitions: readonly [{
212
210
  value?: number | undefined;
213
211
  } | undefined;
214
212
  } | undefined;
213
+ event?: string | undefined;
214
+ achievementRuleId?: string | undefined;
215
215
  uniqueReferee?: boolean | undefined;
216
216
  }>, "many">;
217
217
  badgeTypeId: import("zod").ZodOptional<import("zod").ZodString>;
@@ -386,8 +386,6 @@ export declare const achievementToolDefinitions: readonly [{
386
386
  description?: string | undefined;
387
387
  }> | undefined;
388
388
  conditions?: Record<string, unknown>[] | undefined;
389
- event?: string | undefined;
390
- achievementRuleId?: string | undefined;
391
389
  limit?: {
392
390
  value?: number | undefined;
393
391
  interval?: {
@@ -395,6 +393,8 @@ export declare const achievementToolDefinitions: readonly [{
395
393
  value?: number | undefined;
396
394
  } | undefined;
397
395
  } | undefined;
396
+ event?: string | undefined;
397
+ achievementRuleId?: string | undefined;
398
398
  uniqueReferee?: boolean | undefined;
399
399
  }, {
400
400
  type: "direct" | "referral";
@@ -416,8 +416,6 @@ export declare const achievementToolDefinitions: readonly [{
416
416
  description?: string | undefined;
417
417
  }> | undefined;
418
418
  conditions?: Record<string, unknown>[] | undefined;
419
- event?: string | undefined;
420
- achievementRuleId?: string | undefined;
421
419
  limit?: {
422
420
  value?: number | undefined;
423
421
  interval?: {
@@ -425,6 +423,8 @@ export declare const achievementToolDefinitions: readonly [{
425
423
  value?: number | undefined;
426
424
  } | undefined;
427
425
  } | undefined;
426
+ event?: string | undefined;
427
+ achievementRuleId?: string | undefined;
428
428
  uniqueReferee?: boolean | undefined;
429
429
  }>, "many">;
430
430
  badgeTypeId: import("zod").ZodOptional<import("zod").ZodString>;
@@ -16,17 +16,16 @@ export const achievementToolDefinitions = [
16
16
  {
17
17
  name: "ol_achievement_create",
18
18
  title: "Create Achievement",
19
- description: "Create achievement with rules that track member progress. " +
20
- " TWO MAIN PATTERNS: " +
21
- "1️⃣ ONE-TIME BADGE (e.g., 'First Purchase', 'Welcome'): " +
22
- " Set achievement-level limit: { value: 1 } (earn once ever). " +
23
- " Rule completeRule: { periodGoal: 1, period: { type: 'day', consecutive: 1 } }. " +
24
- " NO rule-level limit needed. " +
25
- "2️⃣ STREAK BADGE (e.g., '7-day streak'): " +
26
- "• Rule completeRule: { periodGoal: 1, period: { type: 'calendarDays', consecutive: 7 } }. " +
27
- " Rule limit: { value: 1, interval: { type: 'calendarDays', value: 1 } } (count 1 per day). " +
28
- "REQUIRED in each rule: type='direct', trigger (transaction/custom_event/etc), aggregation: { type: 'quantity' }, completeRule. " +
29
- "⚠️ COMMON MISTAKE: Using streak pattern for one-time badges. For one-time, use consecutive=1 with NO rule limit.",
19
+ description: "Create an achievement that tracks member progress toward a goal. " +
20
+ "KEY DECISIONS (confirm with user if not specified): " +
21
+ "1. Achievement type -- one-time badge (earn once forever), streak (consecutive days/weeks), or repeatable milestone? " +
22
+ "2. Trigger event -- what counts? transaction, custom_event, reward_redemption, referral, etc. " +
23
+ "3. Goal -- how many events to complete? (e.g., 5 purchases, 7 consecutive days) " +
24
+ "4. Repeatability -- can member earn this more than once? (set limit.value=1 for one-time) " +
25
+ "PATTERNS: " +
26
+ "ONE-TIME: period: { type: 'day', consecutive: 1 }, limit: { value: 1 }, NO rule-level limit. " +
27
+ "STREAK: period: { type: 'calendarDays', consecutive: 7 }, rule limit: { value: 1, interval: { type: 'calendarDays', value: 1 } }. " +
28
+ "Every rule requires: type='direct', aggregation: { type: 'quantity' }, completeRule with periodGoal.",
30
29
  readOnly: false,
31
30
  inputSchema: AchievementCreateInputSchema,
32
31
  handler: achievementCreate,
@@ -159,8 +159,6 @@ export declare const AchievementCreateInputSchema: {
159
159
  description?: string | undefined;
160
160
  }> | undefined;
161
161
  conditions?: Record<string, unknown>[] | undefined;
162
- event?: string | undefined;
163
- achievementRuleId?: string | undefined;
164
162
  limit?: {
165
163
  value?: number | undefined;
166
164
  interval?: {
@@ -168,6 +166,8 @@ export declare const AchievementCreateInputSchema: {
168
166
  value?: number | undefined;
169
167
  } | undefined;
170
168
  } | undefined;
169
+ event?: string | undefined;
170
+ achievementRuleId?: string | undefined;
171
171
  uniqueReferee?: boolean | undefined;
172
172
  }, {
173
173
  type: "direct" | "referral";
@@ -189,8 +189,6 @@ export declare const AchievementCreateInputSchema: {
189
189
  description?: string | undefined;
190
190
  }> | undefined;
191
191
  conditions?: Record<string, unknown>[] | undefined;
192
- event?: string | undefined;
193
- achievementRuleId?: string | undefined;
194
192
  limit?: {
195
193
  value?: number | undefined;
196
194
  interval?: {
@@ -198,6 +196,8 @@ export declare const AchievementCreateInputSchema: {
198
196
  value?: number | undefined;
199
197
  } | undefined;
200
198
  } | undefined;
199
+ event?: string | undefined;
200
+ achievementRuleId?: string | undefined;
201
201
  uniqueReferee?: boolean | undefined;
202
202
  }>, "many">;
203
203
  badgeTypeId: z.ZodOptional<z.ZodString>;
@@ -360,8 +360,6 @@ export declare const AchievementUpdateInputSchema: {
360
360
  description?: string | undefined;
361
361
  }> | undefined;
362
362
  conditions?: Record<string, unknown>[] | undefined;
363
- event?: string | undefined;
364
- achievementRuleId?: string | undefined;
365
363
  limit?: {
366
364
  value?: number | undefined;
367
365
  interval?: {
@@ -369,6 +367,8 @@ export declare const AchievementUpdateInputSchema: {
369
367
  value?: number | undefined;
370
368
  } | undefined;
371
369
  } | undefined;
370
+ event?: string | undefined;
371
+ achievementRuleId?: string | undefined;
372
372
  uniqueReferee?: boolean | undefined;
373
373
  }, {
374
374
  type: "direct" | "referral";
@@ -390,8 +390,6 @@ export declare const AchievementUpdateInputSchema: {
390
390
  description?: string | undefined;
391
391
  }> | undefined;
392
392
  conditions?: Record<string, unknown>[] | undefined;
393
- event?: string | undefined;
394
- achievementRuleId?: string | undefined;
395
393
  limit?: {
396
394
  value?: number | undefined;
397
395
  interval?: {
@@ -399,6 +397,8 @@ export declare const AchievementUpdateInputSchema: {
399
397
  value?: number | undefined;
400
398
  } | undefined;
401
399
  } | undefined;
400
+ event?: string | undefined;
401
+ achievementRuleId?: string | undefined;
402
402
  uniqueReferee?: boolean | undefined;
403
403
  }>, "many">;
404
404
  badgeTypeId: z.ZodOptional<z.ZodString>;