@open-loyalty/mcp-server 1.0.3 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -177
- package/dist/auth/provider.js +2 -14
- package/dist/auth/storage.js +22 -0
- package/dist/client/http.d.ts +5 -0
- package/dist/client/http.js +62 -3
- package/dist/config.d.ts +6 -5
- package/dist/config.js +15 -11
- package/dist/http.js +170 -65
- package/dist/instructions.d.ts +5 -0
- package/dist/instructions.js +420 -0
- package/dist/prompts/fan-engagement-setup.d.ts +107 -0
- package/dist/prompts/fan-engagement-setup.js +492 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +68 -278
- package/dist/tools/achievement/handlers.d.ts +117 -0
- package/dist/tools/achievement/handlers.js +161 -0
- package/dist/tools/achievement/index.d.ts +479 -0
- package/dist/tools/achievement/index.js +74 -0
- package/dist/tools/achievement/schemas.d.ts +433 -0
- package/dist/tools/achievement/schemas.js +142 -0
- package/dist/tools/achievement.d.ts +155 -121
- package/dist/tools/achievement.js +82 -39
- package/dist/tools/admin.d.ts +18 -6
- package/dist/tools/admin.js +24 -12
- package/dist/tools/analytics.d.ts +29 -11
- package/dist/tools/analytics.js +58 -48
- package/dist/tools/apikey.d.ts +10 -3
- package/dist/tools/apikey.js +13 -6
- package/dist/tools/audit.d.ts +6 -2
- package/dist/tools/audit.js +8 -4
- package/dist/tools/badge.d.ts +14 -6
- package/dist/tools/badge.js +36 -27
- package/dist/tools/campaign/handlers.d.ts +42 -0
- package/dist/tools/campaign/handlers.js +223 -0
- package/dist/tools/campaign/index.d.ts +783 -0
- package/dist/tools/campaign/index.js +112 -0
- package/dist/tools/campaign/member-handlers.d.ts +60 -0
- package/dist/tools/campaign/member-handlers.js +159 -0
- package/dist/tools/campaign/schemas.d.ts +704 -0
- package/dist/tools/campaign/schemas.js +259 -0
- package/dist/tools/campaign/types.d.ts +161 -0
- package/dist/tools/campaign/types.js +2 -0
- package/dist/tools/campaign.d.ts +41 -16
- package/dist/tools/campaign.js +38 -25
- package/dist/tools/custom-event.d.ts +315 -0
- package/dist/tools/custom-event.js +270 -0
- package/dist/tools/export.d.ts +12 -4
- package/dist/tools/export.js +25 -20
- package/dist/tools/import.d.ts +9 -3
- package/dist/tools/import.js +33 -21
- package/dist/tools/index.d.ts +3 -11
- package/dist/tools/index.js +17 -475
- package/dist/tools/member/handlers.d.ts +111 -0
- package/dist/tools/member/handlers.js +206 -0
- package/dist/tools/member/index.d.ts +169 -0
- package/dist/tools/member/index.js +92 -0
- package/dist/tools/member/schemas.d.ts +89 -0
- package/dist/tools/member/schemas.js +65 -0
- package/dist/tools/member.d.ts +21 -0
- package/dist/tools/member.js +56 -62
- package/dist/tools/points.d.ts +19 -6
- package/dist/tools/points.js +51 -49
- package/dist/tools/referral/handlers.d.ts +47 -0
- package/dist/tools/referral/handlers.js +115 -0
- package/dist/tools/referral/index.d.ts +44 -0
- package/dist/tools/referral/index.js +44 -0
- package/dist/tools/referral/schemas.d.ts +34 -0
- package/dist/tools/referral/schemas.js +52 -0
- package/dist/tools/reward/handlers.d.ts +110 -0
- package/dist/tools/reward/handlers.js +289 -0
- package/dist/tools/reward/index.d.ts +177 -0
- package/dist/tools/reward/index.js +90 -0
- package/dist/tools/reward/schemas.d.ts +116 -0
- package/dist/tools/reward/schemas.js +91 -0
- package/dist/tools/reward.d.ts +18 -0
- package/dist/tools/reward.js +56 -66
- package/dist/tools/role.d.ts +26 -7
- package/dist/tools/role.js +25 -12
- package/dist/tools/segment/handlers.d.ts +87 -0
- package/dist/tools/segment/handlers.js +174 -0
- package/dist/tools/segment/index.d.ts +395 -0
- package/dist/tools/segment/index.js +87 -0
- package/dist/tools/segment/schemas.d.ts +337 -0
- package/dist/tools/segment/schemas.js +79 -0
- package/dist/tools/segment.d.ts +29 -10
- package/dist/tools/segment.js +84 -50
- package/dist/tools/store.d.ts +12 -4
- package/dist/tools/store.js +16 -8
- package/dist/tools/tierset.d.ts +19 -7
- package/dist/tools/tierset.js +44 -35
- package/dist/tools/transaction.d.ts +16 -8
- package/dist/tools/transaction.js +25 -21
- package/dist/tools/wallet-type.d.ts +7 -3
- package/dist/tools/wallet-type.js +14 -12
- package/dist/tools/webhook.d.ts +23 -10
- package/dist/tools/webhook.js +135 -33
- package/dist/types/schemas/achievement.d.ts +12 -309
- package/dist/types/schemas/achievement.js +0 -13
- package/dist/types/schemas/admin.d.ts +10 -97
- package/dist/types/schemas/admin.js +0 -38
- package/dist/types/schemas/badge.d.ts +0 -37
- package/dist/types/schemas/badge.js +0 -11
- package/dist/types/schemas/campaign.d.ts +64 -832
- package/dist/types/schemas/campaign.js +2 -25
- package/dist/types/schemas/common.d.ts +5 -0
- package/dist/types/schemas/common.js +5 -0
- package/dist/types/schemas/export.d.ts +0 -17
- package/dist/types/schemas/export.js +0 -7
- package/dist/types/schemas/member.d.ts +37 -176
- package/dist/types/schemas/member.js +0 -27
- package/dist/types/schemas/points.d.ts +0 -63
- package/dist/types/schemas/points.js +0 -22
- package/dist/types/schemas/reward.d.ts +71 -68
- package/dist/types/schemas/reward.js +8 -28
- package/dist/types/schemas/role.d.ts +0 -100
- package/dist/types/schemas/role.js +0 -29
- package/dist/types/schemas/segment.d.ts +0 -58
- package/dist/types/schemas/segment.js +0 -17
- package/dist/types/schemas/tierset.d.ts +0 -176
- package/dist/types/schemas/tierset.js +0 -27
- package/dist/types/schemas/transaction.d.ts +23 -254
- package/dist/types/schemas/transaction.js +0 -7
- package/dist/types/schemas/wallet-type.d.ts +8 -8
- package/dist/types/schemas/wallet-type.js +1 -1
- package/dist/types/schemas/webhook.d.ts +0 -58
- package/dist/types/schemas/webhook.js +0 -12
- package/dist/utils/errors.js +30 -3
- package/dist/utils/payload.d.ts +12 -0
- package/dist/utils/payload.js +14 -0
- package/dist/workflows/app-login-streak.d.ts +39 -0
- package/dist/workflows/app-login-streak.js +298 -0
- package/dist/workflows/early-arrival.d.ts +33 -0
- package/dist/workflows/early-arrival.js +148 -0
- package/dist/workflows/index.d.ts +101 -0
- package/dist/workflows/index.js +208 -0
- package/dist/workflows/match-attendance.d.ts +45 -0
- package/dist/workflows/match-attendance.js +308 -0
- package/dist/workflows/sportsbar-visit.d.ts +41 -0
- package/dist/workflows/sportsbar-visit.js +284 -0
- package/dist/workflows/vod-watching.d.ts +43 -0
- package/dist/workflows/vod-watching.js +326 -0
- package/package.json +10 -2
package/dist/server.js
CHANGED
|
@@ -1,290 +1,57 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import {
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getAllTools, getToolHandler } from "./tools/index.js";
|
|
3
4
|
import { OpenLoyaltyError } from "./utils/errors.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Achievement (gamification goals)
|
|
40
|
-
└── Trigger (transaction, custom_event, points_transfer, referral, login)
|
|
41
|
-
└── CompleteRule (periodGoal, period type, unique counting)
|
|
42
|
-
└── Badge (visual reward when completed)
|
|
43
|
-
|
|
44
|
-
Badge (visual recognition)
|
|
45
|
-
└── Linked to achievements via badgeTypeId
|
|
46
|
-
└── Auto-created when referenced by achievement
|
|
47
|
-
|
|
48
|
-
## Key Workflows
|
|
49
|
-
|
|
50
|
-
### 1. Create 3-tier Loyalty Program:
|
|
51
|
-
wallet_type_list → tierset_create → tierset_get → tierset_update_tiers
|
|
52
|
-
|
|
53
|
-
### 2. Register and Reward Member:
|
|
54
|
-
member_create → points_add (welcome bonus) → member_get (verify)
|
|
55
|
-
|
|
56
|
-
### 3. Record Purchase and Earn Points:
|
|
57
|
-
transaction_create (with customerData) → triggers campaigns → points auto-added
|
|
58
|
-
|
|
59
|
-
### 4. Redeem Reward:
|
|
60
|
-
reward_list → reward_buy (deducts points) → reward_redeem (mark coupon used)
|
|
61
|
-
|
|
62
|
-
### 5. Assign Unmatched Transaction:
|
|
63
|
-
transaction_list (matched=false) → transaction_assign_member → points earned
|
|
64
|
-
|
|
65
|
-
### 6. Check Member Tier Progress:
|
|
66
|
-
member_get_tier_progress → shows current tier, next tier, progress %
|
|
67
|
-
|
|
68
|
-
### 7. Double Points for VIP Members:
|
|
69
|
-
segment_create (with tier criteria for VIP level) → campaign_create (targeting that segment with give_points effect and multiplier: 2)
|
|
70
|
-
|
|
71
|
-
### 8. Purchase Achievement with Badge:
|
|
72
|
-
badge_list (find or note badgeTypeId) → achievement_create (trigger: transaction, periodGoal for amount, badgeTypeId)
|
|
73
|
-
|
|
74
|
-
### 9. Create Targeted Promotion:
|
|
75
|
-
segment_create (define audience) → campaign_create (type: earning, target: segment, segmentIds: [segmentId])
|
|
76
|
-
|
|
77
|
-
### 10. Track Member Gamification:
|
|
78
|
-
achievement_list_member_achievements → badge_get_member_badges → achievement_get_member_progress
|
|
79
|
-
|
|
80
|
-
## Discovery Paths:
|
|
81
|
-
- Members: member_list → member_get → points_get_balance
|
|
82
|
-
- Tiers: tierset_list → tierset_get → tierset_get_tiers
|
|
83
|
-
- Rewards: reward_category_list → reward_list → reward_get
|
|
84
|
-
- Transactions: transaction_list → transaction_get
|
|
85
|
-
- Campaigns: campaign_list → campaign_get → campaign_simulate
|
|
86
|
-
- Segments: segment_list → segment_get → segment_get_members → campaign_create (audience targeting)
|
|
87
|
-
- Achievements: achievement_list → badge_list → achievement_create (with badge)
|
|
88
|
-
- Member Gamification: achievement_list_member_achievements → badge_get_member_badges
|
|
89
|
-
|
|
90
|
-
## Important Patterns:
|
|
91
|
-
- conditionId REQUIRED for tier thresholds - call tierset_get after tierset_create
|
|
92
|
-
- Points operations: check balance with points_get_balance before spending
|
|
93
|
-
- Reward flow: reward_buy returns couponCode, use reward_redeem to mark used
|
|
94
|
-
- Transactions auto-match to members via customerData (email, phone, loyaltyCardNumber)
|
|
95
|
-
|
|
96
|
-
### Pagination:
|
|
97
|
-
- Traditional pagination: use page + perPage params
|
|
98
|
-
- Cursor pagination: provide cursor from previous response for efficient deep pagination
|
|
99
|
-
- Cursor-enabled tools: member_list, transaction_list, points_get_history
|
|
100
|
-
- First scroll request: cursor="" (empty string)
|
|
101
|
-
- Subsequent requests: use cursor value from previous response
|
|
102
|
-
- When cursor provided, page param is ignored
|
|
103
|
-
|
|
104
|
-
### Campaign Patterns:
|
|
105
|
-
- Campaign types: earning (earn points), spending (spend points), custom (custom events), instant_reward
|
|
106
|
-
- Triggers: transaction (purchase), custom_event, points_transfer, referral, registration
|
|
107
|
-
- Effects: give_points, multiply_points, percentage_discount, fixed_discount
|
|
108
|
-
- Target campaigns to segments or tiers using target: "segment" and segmentIds array
|
|
109
|
-
|
|
110
|
-
### Segment Patterns:
|
|
111
|
-
- Parts use OR logic (member matches ANY part)
|
|
112
|
-
- Criteria within a part use AND logic (member must match ALL criteria in that part)
|
|
113
|
-
- Common criteria: purchase_period (spending), transaction_count, average_transaction, anniversary, last_purchase_days, tier
|
|
114
|
-
|
|
115
|
-
### Achievement Patterns:
|
|
116
|
-
- Triggers: transaction, custom_event, points_transfer, reward_redemption, referral, achievement, tier_change, registration, profile_update, login
|
|
117
|
-
- periodGoal sets the target (e.g., 5 purchases, 1000 points spent)
|
|
118
|
-
- period defines timeframe: day, week, month, year, forever
|
|
119
|
-
- Link to badge via badgeTypeId - badge auto-created if doesn't exist
|
|
120
|
-
- uniqueAttribute for counting distinct values (e.g., unique products)
|
|
121
|
-
|
|
122
|
-
## Phase 3: Analytics, Admin, Roles & Stores
|
|
123
|
-
|
|
124
|
-
### Domain Model (Extended)
|
|
125
|
-
|
|
126
|
-
Admin (system user)
|
|
127
|
-
└── Roles (permission sets)
|
|
128
|
-
└── Permissions (resource + access level)
|
|
129
|
-
└── API Keys (programmatic access)
|
|
130
|
-
|
|
131
|
-
Store (multi-tenant container)
|
|
132
|
-
└── Members, Campaigns, Rewards (isolated per store)
|
|
133
|
-
|
|
134
|
-
Audit Log (compliance tracking)
|
|
135
|
-
└── eventType, entityType, entityId, username, timestamp
|
|
136
|
-
|
|
137
|
-
### Analytics Workflows:
|
|
138
|
-
|
|
139
|
-
#### 11. Get Program Overview:
|
|
140
|
-
analytics_dashboard → analytics_tiers → analytics_members
|
|
141
|
-
|
|
142
|
-
#### 12. Analyze Points Economy:
|
|
143
|
-
analytics_points → analytics_units (per wallet) → points_get_histogram
|
|
144
|
-
|
|
145
|
-
#### 13. Measure Campaign Performance:
|
|
146
|
-
analytics_campaigns → analytics_campaign_detail (specific campaign)
|
|
147
|
-
|
|
148
|
-
#### 14. Track Transactions:
|
|
149
|
-
analytics_transactions → transaction_list (details)
|
|
150
|
-
|
|
151
|
-
### Aggregation Queries (Top Spenders, Purchase Analysis):
|
|
152
|
-
|
|
153
|
-
IMPORTANT: For queries like "top 5 spenders in July 2025", use this approach:
|
|
154
|
-
|
|
155
|
-
#### 15. Find Top Spenders by Date Range:
|
|
156
|
-
1. transaction_list(purchasedAtFrom, purchasedAtTo, perPage=50, cursor='') - returns customerId with each transaction
|
|
157
|
-
2. Use cursor pagination to fetch ALL pages - even if there are 1000+ transactions
|
|
158
|
-
3. Aggregate grossValue by customerId in your code
|
|
159
|
-
4. Sort by total spent, take top N
|
|
160
|
-
5. member_get for each top spender to get names/details
|
|
161
|
-
|
|
162
|
-
CRITICAL - DO NOT TRY TO BE SMART OR OPTIMIZE:
|
|
163
|
-
- ALWAYS iterate through ALL pages using cursor pagination - this is the ONLY correct approach
|
|
164
|
-
- DO NOT skip pages or try to sample data - you will get inaccurate results
|
|
165
|
-
- DO NOT use transaction_get individually - transaction_list already includes customerId
|
|
166
|
-
- DO NOT try to find "smarter" analytics endpoints - they don't exist for per-customer aggregation
|
|
167
|
-
- Large datasets (1000+ transactions) are normal - just keep paginating until cursor is empty
|
|
168
|
-
- Start with cursor='' (empty string), use returned cursor for next request, repeat until done
|
|
169
|
-
|
|
170
|
-
Example: Top 5 spenders July 2025
|
|
171
|
-
- First request: transaction_list(purchasedAtFrom: "2025-07-01", purchasedAtTo: "2025-07-31", perPage: 50, cursor: "")
|
|
172
|
-
- Each result includes: transactionId, grossValue, customerId, purchasedAt
|
|
173
|
-
- Keep fetching with returned cursor until response has no cursor or empty transactions
|
|
174
|
-
- Even if there are 1500 transactions (30 pages), iterate through ALL of them
|
|
175
|
-
- Group by customerId, sum grossValue, sort descending, take 5
|
|
176
|
-
|
|
177
|
-
### Admin & Security Workflows:
|
|
178
|
-
|
|
179
|
-
#### 15. Create Admin with Limited Role:
|
|
180
|
-
acl_get_resources → role_create → admin_create (with roleId)
|
|
181
|
-
|
|
182
|
-
#### 16. Generate API Key for Integration:
|
|
183
|
-
admin_get → apikey_create → SAVE TOKEN IMMEDIATELY (shown once!)
|
|
184
|
-
|
|
185
|
-
#### 17. Audit User Actions:
|
|
186
|
-
audit_list (filter by username/entity) → audit_export (compliance report)
|
|
187
|
-
|
|
188
|
-
### Store Multi-Tenancy:
|
|
189
|
-
|
|
190
|
-
#### 18. Create New Store:
|
|
191
|
-
store_create → configure campaigns/tiers → member_create (with storeCode)
|
|
192
|
-
|
|
193
|
-
### Analytics Query Patterns:
|
|
194
|
-
- All analytics tools support dateFrom/dateTo ISO date filters
|
|
195
|
-
- analytics_dashboard: high-level program metrics
|
|
196
|
-
- analytics_units: wallet-specific metrics (requires walletTypeCode)
|
|
197
|
-
- analytics_campaign_detail: detailed metrics for single campaign
|
|
198
|
-
|
|
199
|
-
### Admin → Role → Permission Model:
|
|
200
|
-
┌─────────┐ ┌──────┐ ┌────────────┐
|
|
201
|
-
│ Admin │────▶│ Role │────▶│ Permission │
|
|
202
|
-
└─────────┘ └──────┘ └────────────┘
|
|
203
|
-
│ │
|
|
204
|
-
│ resource + access
|
|
205
|
-
│ (VIEW, MODIFY, etc.)
|
|
206
|
-
▼
|
|
207
|
-
┌─────────┐
|
|
208
|
-
│ API Key │
|
|
209
|
-
└─────────┘
|
|
210
|
-
|
|
211
|
-
### Store Multi-Tenancy:
|
|
212
|
-
- Each store is isolated: members, campaigns, rewards, transactions
|
|
213
|
-
- storeCode parameter routes requests to correct tenant
|
|
214
|
-
- Default store used when storeCode omitted
|
|
215
|
-
|
|
216
|
-
## Phase 4: Webhooks, Import & Export
|
|
217
|
-
|
|
218
|
-
### Domain Model (Extended)
|
|
219
|
-
|
|
220
|
-
Webhook Subscription (event notification)
|
|
221
|
-
└── eventName (event to subscribe to)
|
|
222
|
-
└── url (callback endpoint)
|
|
223
|
-
└── headers (custom HTTP headers)
|
|
224
|
-
|
|
225
|
-
Import (bulk data upload)
|
|
226
|
-
└── type: member, groupValue, segmentMembers, unitTransferAdding, etc.
|
|
227
|
-
└── status: pending, processing, succeed, failed
|
|
228
|
-
└── items (individual row results)
|
|
229
|
-
|
|
230
|
-
Export (bulk data download)
|
|
231
|
-
└── type: campaignCode, member, memberTier, memberSegment, rewardFulfillment
|
|
232
|
-
└── status: pending, done, failed, error
|
|
233
|
-
└── CSV file (when status='done')
|
|
234
|
-
|
|
235
|
-
### Webhook Workflows:
|
|
236
|
-
|
|
237
|
-
#### 19. Subscribe to Member Events for CRM Sync:
|
|
238
|
-
webhook_events → webhook_create (eventName: 'member.created', url: 'https://crm.example.com/webhook')
|
|
239
|
-
|
|
240
|
-
#### 20. List and Manage Subscriptions:
|
|
241
|
-
webhook_list → webhook_get → webhook_update or webhook_delete
|
|
242
|
-
|
|
243
|
-
### Import Workflows:
|
|
244
|
-
|
|
245
|
-
#### 21. Bulk Import Members from CSV:
|
|
246
|
-
import_create (type: 'member', fileContent: CSV data) → import_list → import_get (check status)
|
|
247
|
-
|
|
248
|
-
#### 22. Bulk Add Points to Members:
|
|
249
|
-
import_create (type: 'unitTransferAdding', fileContent: CSV) → poll import_get until complete
|
|
250
|
-
|
|
251
|
-
### Export Workflows:
|
|
252
|
-
|
|
253
|
-
#### 23. Export Campaign Codes:
|
|
254
|
-
export_create (type: 'campaignCode', filters: { campaignId }) → poll export_get (until status='done') → export_download
|
|
255
|
-
|
|
256
|
-
#### 24. Export Member Data:
|
|
257
|
-
export_create (type: 'member') → export_get → export_download (returns CSV)
|
|
258
|
-
|
|
259
|
-
### Webhook Patterns:
|
|
260
|
-
- Use webhook_events to discover available event types before subscribing
|
|
261
|
-
- API uses wrapper: { webhookSubscription: { eventName, url, headers? } }
|
|
262
|
-
- Common events: member.created, member.updated, transaction.created, reward.purchased
|
|
263
|
-
|
|
264
|
-
### Import Patterns:
|
|
265
|
-
- Import is async: create returns importId, poll status with import_get
|
|
266
|
-
- CSV format required - provide plain text, not base64
|
|
267
|
-
- Types: member, groupValue, segmentMembers, unitTransferAdding, unitTransferSpending, transaction, campaignCode, rewardCoupon
|
|
268
|
-
|
|
269
|
-
### Export Patterns:
|
|
270
|
-
- Export is async: create returns exportId, poll status until 'done'
|
|
271
|
-
- API body wrapper varies by type: { campaignCode: { filters... } }
|
|
272
|
-
- Only call export_download when status='done'
|
|
273
|
-
- Types: campaignCode, member, memberTier, memberSegment, rewardFulfillment
|
|
274
|
-
`;
|
|
5
|
+
import { SERVER_INSTRUCTIONS } from "./instructions.js";
|
|
6
|
+
/**
|
|
7
|
+
* Convert an inputSchema object (Record<string, ZodType>) to a Zod object schema.
|
|
8
|
+
* This allows centralized validation of all tool inputs.
|
|
9
|
+
*/
|
|
10
|
+
function createZodSchema(inputSchema) {
|
|
11
|
+
return z.object(inputSchema);
|
|
12
|
+
}
|
|
13
|
+
const TOOL_WRAPPER_KEYS = {
|
|
14
|
+
ol_tierset_create: "tierSet",
|
|
15
|
+
ol_tierset_update: "tierSet",
|
|
16
|
+
ol_campaign_create: "campaign",
|
|
17
|
+
ol_campaign_update: "campaign",
|
|
18
|
+
};
|
|
19
|
+
function normalizeToolInput(toolName, args) {
|
|
20
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) {
|
|
21
|
+
return args;
|
|
22
|
+
}
|
|
23
|
+
const wrapperKey = TOOL_WRAPPER_KEYS[toolName];
|
|
24
|
+
if (!wrapperKey) {
|
|
25
|
+
return args;
|
|
26
|
+
}
|
|
27
|
+
const record = args;
|
|
28
|
+
const wrapped = record[wrapperKey];
|
|
29
|
+
if (!wrapped || typeof wrapped !== "object" || Array.isArray(wrapped)) {
|
|
30
|
+
return args;
|
|
31
|
+
}
|
|
32
|
+
const { [wrapperKey]: _wrapper, ...rest } = record;
|
|
33
|
+
const merged = {
|
|
34
|
+
...rest,
|
|
35
|
+
...wrapped,
|
|
36
|
+
};
|
|
37
|
+
return merged;
|
|
38
|
+
}
|
|
275
39
|
export function createServer() {
|
|
276
40
|
const server = new McpServer({
|
|
277
41
|
name: "openloyalty",
|
|
278
|
-
version: "1.
|
|
42
|
+
version: "1.3.0",
|
|
279
43
|
});
|
|
280
44
|
const tools = getAllTools();
|
|
281
45
|
for (const tool of tools) {
|
|
282
|
-
const metadata = toolMetadata[tool.name];
|
|
283
46
|
server.registerTool(tool.name, {
|
|
284
|
-
title:
|
|
47
|
+
title: tool.title,
|
|
285
48
|
description: tool.description,
|
|
286
49
|
inputSchema: tool.inputSchema,
|
|
287
|
-
annotations:
|
|
50
|
+
annotations: {
|
|
51
|
+
readOnlyHint: tool.readOnly,
|
|
52
|
+
destructiveHint: tool.destructive,
|
|
53
|
+
openWorldHint: true,
|
|
54
|
+
},
|
|
288
55
|
}, async (args) => {
|
|
289
56
|
const handler = getToolHandler(tool.name);
|
|
290
57
|
if (!handler) {
|
|
@@ -299,12 +66,35 @@ export function createServer() {
|
|
|
299
66
|
};
|
|
300
67
|
}
|
|
301
68
|
try {
|
|
302
|
-
|
|
69
|
+
// Validate input against the tool's Zod schema
|
|
70
|
+
const schema = createZodSchema(tool.inputSchema);
|
|
71
|
+
let parseResult = schema.safeParse(args);
|
|
72
|
+
if (!parseResult.success) {
|
|
73
|
+
const normalizedArgs = normalizeToolInput(tool.name, args);
|
|
74
|
+
if (normalizedArgs !== args) {
|
|
75
|
+
const retryResult = schema.safeParse(normalizedArgs);
|
|
76
|
+
if (retryResult.success) {
|
|
77
|
+
parseResult = retryResult;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!parseResult.success) {
|
|
82
|
+
const issues = parseResult.error.issues
|
|
83
|
+
.map((i) => `${i.path.join(".") || "input"}: ${i.message}`)
|
|
84
|
+
.join("; ");
|
|
85
|
+
throw new OpenLoyaltyError({
|
|
86
|
+
code: "VALIDATION_ERROR",
|
|
87
|
+
message: "Invalid input parameters",
|
|
88
|
+
hint: `Fix the following validation errors: ${issues}`,
|
|
89
|
+
relatedTool: tool.name,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const result = await handler(parseResult.data);
|
|
303
93
|
return {
|
|
304
94
|
content: [
|
|
305
95
|
{
|
|
306
96
|
type: "text",
|
|
307
|
-
text: result === undefined ? "Success" : JSON.stringify(result, null, 2),
|
|
97
|
+
text: result === undefined ? "Success" : JSON.stringify(result, null, process.env.MCP_DEBUG === "true" ? 2 : undefined),
|
|
308
98
|
},
|
|
309
99
|
],
|
|
310
100
|
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { AchievementListItem } from "../../types/schemas/achievement.js";
|
|
2
|
+
export declare function achievementList(input: {
|
|
3
|
+
storeCode?: string;
|
|
4
|
+
page?: number;
|
|
5
|
+
perPage?: number;
|
|
6
|
+
active?: boolean;
|
|
7
|
+
name?: string;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
achievements: AchievementListItem[];
|
|
10
|
+
total: {
|
|
11
|
+
all?: number;
|
|
12
|
+
filtered?: number;
|
|
13
|
+
};
|
|
14
|
+
}>;
|
|
15
|
+
interface AchievementCreateInput {
|
|
16
|
+
storeCode?: string;
|
|
17
|
+
translations: Record<string, {
|
|
18
|
+
name: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
}>;
|
|
21
|
+
active?: boolean;
|
|
22
|
+
activity?: {
|
|
23
|
+
startsAt?: string;
|
|
24
|
+
endsAt?: string;
|
|
25
|
+
operator?: string;
|
|
26
|
+
};
|
|
27
|
+
limit?: {
|
|
28
|
+
value?: number;
|
|
29
|
+
interval?: {
|
|
30
|
+
type: string;
|
|
31
|
+
value?: number;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
rules: Array<Record<string, unknown>>;
|
|
35
|
+
badgeTypeId?: string;
|
|
36
|
+
}
|
|
37
|
+
export declare function achievementCreate(input: AchievementCreateInput): Promise<{
|
|
38
|
+
achievementId: string;
|
|
39
|
+
}>;
|
|
40
|
+
export declare function achievementGet(input: {
|
|
41
|
+
storeCode?: string;
|
|
42
|
+
achievementId: string;
|
|
43
|
+
}): Promise<Record<string, unknown>>;
|
|
44
|
+
interface AchievementUpdateInput {
|
|
45
|
+
storeCode?: string;
|
|
46
|
+
achievementId: string;
|
|
47
|
+
translations: Record<string, {
|
|
48
|
+
name: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
}>;
|
|
51
|
+
active?: boolean;
|
|
52
|
+
activity?: {
|
|
53
|
+
startsAt?: string;
|
|
54
|
+
endsAt?: string;
|
|
55
|
+
operator?: string;
|
|
56
|
+
};
|
|
57
|
+
limit?: {
|
|
58
|
+
value?: number;
|
|
59
|
+
interval?: {
|
|
60
|
+
type: string;
|
|
61
|
+
value?: number;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
rules: Array<Record<string, unknown>>;
|
|
65
|
+
badgeTypeId?: string;
|
|
66
|
+
}
|
|
67
|
+
export declare function achievementUpdate(input: AchievementUpdateInput): Promise<void>;
|
|
68
|
+
interface AchievementPatchInput {
|
|
69
|
+
storeCode?: string;
|
|
70
|
+
achievementId: string;
|
|
71
|
+
active?: boolean;
|
|
72
|
+
translations?: Record<string, {
|
|
73
|
+
name?: string;
|
|
74
|
+
description?: string;
|
|
75
|
+
}>;
|
|
76
|
+
}
|
|
77
|
+
export declare function achievementPatch(input: AchievementPatchInput): Promise<void>;
|
|
78
|
+
export interface AchievementMemberProgress {
|
|
79
|
+
achievementId: string;
|
|
80
|
+
achievementName?: string;
|
|
81
|
+
achievementDescription?: string;
|
|
82
|
+
limitReached?: boolean;
|
|
83
|
+
memberProgress: {
|
|
84
|
+
completedCount: number;
|
|
85
|
+
rules: Array<{
|
|
86
|
+
achievementRuleId: string;
|
|
87
|
+
periodGoal: number;
|
|
88
|
+
currentPeriodValue: number;
|
|
89
|
+
consecutivePeriods?: number;
|
|
90
|
+
completedConsecutivePeriods?: number;
|
|
91
|
+
periodType?: string;
|
|
92
|
+
type?: string;
|
|
93
|
+
trigger?: string;
|
|
94
|
+
periodValue?: number;
|
|
95
|
+
}>;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export declare function achievementGetMemberProgress(input: {
|
|
99
|
+
storeCode?: string;
|
|
100
|
+
memberId: string;
|
|
101
|
+
achievementId: string;
|
|
102
|
+
}): Promise<AchievementMemberProgress>;
|
|
103
|
+
export declare function achievementListMemberAchievements(input: {
|
|
104
|
+
storeCode?: string;
|
|
105
|
+
memberId: string;
|
|
106
|
+
page?: number;
|
|
107
|
+
perPage?: number;
|
|
108
|
+
name?: string;
|
|
109
|
+
achievementId?: string;
|
|
110
|
+
}): Promise<{
|
|
111
|
+
achievements: AchievementMemberProgress[];
|
|
112
|
+
total: {
|
|
113
|
+
all?: number;
|
|
114
|
+
filtered?: number;
|
|
115
|
+
};
|
|
116
|
+
}>;
|
|
117
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { apiGet, apiPost, apiPut, apiPatch } from "../../client/http.js";
|
|
2
|
+
import { formatApiError } from "../../utils/errors.js";
|
|
3
|
+
import { getStoreCode } from "../../config.js";
|
|
4
|
+
export async function achievementList(input) {
|
|
5
|
+
const storeCode = getStoreCode(input.storeCode);
|
|
6
|
+
const params = new URLSearchParams();
|
|
7
|
+
if (input.page)
|
|
8
|
+
params.append("_page", String(input.page));
|
|
9
|
+
if (input.perPage)
|
|
10
|
+
params.append("_itemsOnPage", String(input.perPage));
|
|
11
|
+
if (input.active !== undefined)
|
|
12
|
+
params.append("active", String(input.active));
|
|
13
|
+
if (input.name)
|
|
14
|
+
params.append("name", input.name);
|
|
15
|
+
const queryString = params.toString();
|
|
16
|
+
const url = `/${storeCode}/achievement${queryString ? `?${queryString}` : ""}`;
|
|
17
|
+
try {
|
|
18
|
+
const response = await apiGet(url);
|
|
19
|
+
const achievements = (response.items || []).map((item) => ({
|
|
20
|
+
achievementId: item.achievementId,
|
|
21
|
+
name: item.name,
|
|
22
|
+
active: item.active,
|
|
23
|
+
createdAt: item.createdAt,
|
|
24
|
+
badgeTypeId: item.badgeTypeId,
|
|
25
|
+
}));
|
|
26
|
+
const total = response.total || {};
|
|
27
|
+
return {
|
|
28
|
+
achievements,
|
|
29
|
+
total: {
|
|
30
|
+
all: typeof total.all === "number" ? total.all : undefined,
|
|
31
|
+
filtered: typeof total.filtered === "number" ? total.filtered : undefined,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw formatApiError(error, "ol_achievement_list");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function achievementCreate(input) {
|
|
40
|
+
const storeCode = getStoreCode(input.storeCode);
|
|
41
|
+
const achievementPayload = {
|
|
42
|
+
translations: input.translations,
|
|
43
|
+
rules: input.rules,
|
|
44
|
+
};
|
|
45
|
+
if (input.active !== undefined)
|
|
46
|
+
achievementPayload.active = input.active;
|
|
47
|
+
if (input.activity)
|
|
48
|
+
achievementPayload.activity = input.activity;
|
|
49
|
+
if (input.limit)
|
|
50
|
+
achievementPayload.limit = input.limit;
|
|
51
|
+
if (input.badgeTypeId)
|
|
52
|
+
achievementPayload.badgeTypeId = input.badgeTypeId;
|
|
53
|
+
try {
|
|
54
|
+
// CRITICAL: Wrap body as { achievement: {...} }
|
|
55
|
+
const response = await apiPost(`/${storeCode}/achievement`, { achievement: achievementPayload });
|
|
56
|
+
return { achievementId: response.achievementId };
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
throw formatApiError(error, "ol_achievement_create");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export async function achievementGet(input) {
|
|
63
|
+
const storeCode = getStoreCode(input.storeCode);
|
|
64
|
+
try {
|
|
65
|
+
const response = await apiGet(`/${storeCode}/achievement/${input.achievementId}`);
|
|
66
|
+
return response;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
throw formatApiError(error, "ol_achievement_get");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export async function achievementUpdate(input) {
|
|
73
|
+
const storeCode = getStoreCode(input.storeCode);
|
|
74
|
+
const achievementPayload = {
|
|
75
|
+
translations: input.translations,
|
|
76
|
+
rules: input.rules,
|
|
77
|
+
};
|
|
78
|
+
if (input.active !== undefined)
|
|
79
|
+
achievementPayload.active = input.active;
|
|
80
|
+
if (input.activity)
|
|
81
|
+
achievementPayload.activity = input.activity;
|
|
82
|
+
if (input.limit)
|
|
83
|
+
achievementPayload.limit = input.limit;
|
|
84
|
+
if (input.badgeTypeId !== undefined)
|
|
85
|
+
achievementPayload.badgeTypeId = input.badgeTypeId;
|
|
86
|
+
try {
|
|
87
|
+
// CRITICAL: Wrap body as { achievement: {...} }
|
|
88
|
+
await apiPut(`/${storeCode}/achievement/${input.achievementId}`, { achievement: achievementPayload });
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
throw formatApiError(error, "ol_achievement_update");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export async function achievementPatch(input) {
|
|
95
|
+
const storeCode = getStoreCode(input.storeCode);
|
|
96
|
+
const achievementPayload = {};
|
|
97
|
+
if (input.active !== undefined)
|
|
98
|
+
achievementPayload.active = input.active;
|
|
99
|
+
if (input.translations)
|
|
100
|
+
achievementPayload.translations = input.translations;
|
|
101
|
+
try {
|
|
102
|
+
// CRITICAL: Wrap body as { achievement: {...} }
|
|
103
|
+
await apiPatch(`/${storeCode}/achievement/${input.achievementId}`, { achievement: achievementPayload });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
throw formatApiError(error, "ol_achievement_patch");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export async function achievementGetMemberProgress(input) {
|
|
110
|
+
const storeCode = getStoreCode(input.storeCode);
|
|
111
|
+
try {
|
|
112
|
+
// Try the member achievements list endpoint with achievementId filter
|
|
113
|
+
// The direct /achievement/{id} endpoint may not exist
|
|
114
|
+
const response = await apiGet(`/${storeCode}/member/${input.memberId}/achievement?achievementId=${input.achievementId}`);
|
|
115
|
+
// Return first matching achievement
|
|
116
|
+
const achievements = response.items || [];
|
|
117
|
+
if (achievements.length === 0) {
|
|
118
|
+
// If no achievements found, return empty progress
|
|
119
|
+
return {
|
|
120
|
+
achievementId: input.achievementId,
|
|
121
|
+
memberProgress: {
|
|
122
|
+
completedCount: 0,
|
|
123
|
+
rules: [],
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return achievements[0];
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
throw formatApiError(error, "ol_achievement_get_member_progress");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export async function achievementListMemberAchievements(input) {
|
|
134
|
+
const storeCode = getStoreCode(input.storeCode);
|
|
135
|
+
const params = new URLSearchParams();
|
|
136
|
+
if (input.page)
|
|
137
|
+
params.append("_page", String(input.page));
|
|
138
|
+
if (input.perPage)
|
|
139
|
+
params.append("_itemsOnPage", String(input.perPage));
|
|
140
|
+
if (input.name)
|
|
141
|
+
params.append("name", input.name);
|
|
142
|
+
if (input.achievementId)
|
|
143
|
+
params.append("achievementId", input.achievementId);
|
|
144
|
+
const queryString = params.toString();
|
|
145
|
+
const url = `/${storeCode}/member/${input.memberId}/achievement${queryString ? `?${queryString}` : ""}`;
|
|
146
|
+
try {
|
|
147
|
+
const response = await apiGet(url);
|
|
148
|
+
const achievements = response.items || [];
|
|
149
|
+
const total = response.total || {};
|
|
150
|
+
return {
|
|
151
|
+
achievements,
|
|
152
|
+
total: {
|
|
153
|
+
all: typeof total.all === "number" ? total.all : undefined,
|
|
154
|
+
filtered: typeof total.filtered === "number" ? total.filtered : undefined,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
throw formatApiError(error, "ol_achievement_list_member_achievements");
|
|
160
|
+
}
|
|
161
|
+
}
|