@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.
Files changed (142) hide show
  1. package/README.md +180 -177
  2. package/dist/auth/provider.js +2 -14
  3. package/dist/auth/storage.js +22 -0
  4. package/dist/client/http.d.ts +5 -0
  5. package/dist/client/http.js +62 -3
  6. package/dist/config.d.ts +6 -5
  7. package/dist/config.js +15 -11
  8. package/dist/http.js +170 -65
  9. package/dist/instructions.d.ts +5 -0
  10. package/dist/instructions.js +420 -0
  11. package/dist/prompts/fan-engagement-setup.d.ts +107 -0
  12. package/dist/prompts/fan-engagement-setup.js +492 -0
  13. package/dist/server.d.ts +1 -1
  14. package/dist/server.js +68 -278
  15. package/dist/tools/achievement/handlers.d.ts +117 -0
  16. package/dist/tools/achievement/handlers.js +161 -0
  17. package/dist/tools/achievement/index.d.ts +479 -0
  18. package/dist/tools/achievement/index.js +74 -0
  19. package/dist/tools/achievement/schemas.d.ts +433 -0
  20. package/dist/tools/achievement/schemas.js +142 -0
  21. package/dist/tools/achievement.d.ts +155 -121
  22. package/dist/tools/achievement.js +82 -39
  23. package/dist/tools/admin.d.ts +18 -6
  24. package/dist/tools/admin.js +24 -12
  25. package/dist/tools/analytics.d.ts +29 -11
  26. package/dist/tools/analytics.js +58 -48
  27. package/dist/tools/apikey.d.ts +10 -3
  28. package/dist/tools/apikey.js +13 -6
  29. package/dist/tools/audit.d.ts +6 -2
  30. package/dist/tools/audit.js +8 -4
  31. package/dist/tools/badge.d.ts +14 -6
  32. package/dist/tools/badge.js +36 -27
  33. package/dist/tools/campaign/handlers.d.ts +42 -0
  34. package/dist/tools/campaign/handlers.js +223 -0
  35. package/dist/tools/campaign/index.d.ts +783 -0
  36. package/dist/tools/campaign/index.js +112 -0
  37. package/dist/tools/campaign/member-handlers.d.ts +60 -0
  38. package/dist/tools/campaign/member-handlers.js +159 -0
  39. package/dist/tools/campaign/schemas.d.ts +704 -0
  40. package/dist/tools/campaign/schemas.js +259 -0
  41. package/dist/tools/campaign/types.d.ts +161 -0
  42. package/dist/tools/campaign/types.js +2 -0
  43. package/dist/tools/campaign.d.ts +41 -16
  44. package/dist/tools/campaign.js +38 -25
  45. package/dist/tools/custom-event.d.ts +315 -0
  46. package/dist/tools/custom-event.js +270 -0
  47. package/dist/tools/export.d.ts +12 -4
  48. package/dist/tools/export.js +25 -20
  49. package/dist/tools/import.d.ts +9 -3
  50. package/dist/tools/import.js +33 -21
  51. package/dist/tools/index.d.ts +3 -11
  52. package/dist/tools/index.js +17 -475
  53. package/dist/tools/member/handlers.d.ts +111 -0
  54. package/dist/tools/member/handlers.js +206 -0
  55. package/dist/tools/member/index.d.ts +169 -0
  56. package/dist/tools/member/index.js +92 -0
  57. package/dist/tools/member/schemas.d.ts +89 -0
  58. package/dist/tools/member/schemas.js +65 -0
  59. package/dist/tools/member.d.ts +21 -0
  60. package/dist/tools/member.js +56 -62
  61. package/dist/tools/points.d.ts +19 -6
  62. package/dist/tools/points.js +51 -49
  63. package/dist/tools/referral/handlers.d.ts +47 -0
  64. package/dist/tools/referral/handlers.js +115 -0
  65. package/dist/tools/referral/index.d.ts +44 -0
  66. package/dist/tools/referral/index.js +44 -0
  67. package/dist/tools/referral/schemas.d.ts +34 -0
  68. package/dist/tools/referral/schemas.js +52 -0
  69. package/dist/tools/reward/handlers.d.ts +110 -0
  70. package/dist/tools/reward/handlers.js +289 -0
  71. package/dist/tools/reward/index.d.ts +177 -0
  72. package/dist/tools/reward/index.js +90 -0
  73. package/dist/tools/reward/schemas.d.ts +116 -0
  74. package/dist/tools/reward/schemas.js +91 -0
  75. package/dist/tools/reward.d.ts +18 -0
  76. package/dist/tools/reward.js +56 -66
  77. package/dist/tools/role.d.ts +26 -7
  78. package/dist/tools/role.js +25 -12
  79. package/dist/tools/segment/handlers.d.ts +87 -0
  80. package/dist/tools/segment/handlers.js +174 -0
  81. package/dist/tools/segment/index.d.ts +395 -0
  82. package/dist/tools/segment/index.js +87 -0
  83. package/dist/tools/segment/schemas.d.ts +337 -0
  84. package/dist/tools/segment/schemas.js +79 -0
  85. package/dist/tools/segment.d.ts +29 -10
  86. package/dist/tools/segment.js +84 -50
  87. package/dist/tools/store.d.ts +12 -4
  88. package/dist/tools/store.js +16 -8
  89. package/dist/tools/tierset.d.ts +19 -7
  90. package/dist/tools/tierset.js +44 -35
  91. package/dist/tools/transaction.d.ts +16 -8
  92. package/dist/tools/transaction.js +25 -21
  93. package/dist/tools/wallet-type.d.ts +7 -3
  94. package/dist/tools/wallet-type.js +14 -12
  95. package/dist/tools/webhook.d.ts +23 -10
  96. package/dist/tools/webhook.js +135 -33
  97. package/dist/types/schemas/achievement.d.ts +12 -309
  98. package/dist/types/schemas/achievement.js +0 -13
  99. package/dist/types/schemas/admin.d.ts +10 -97
  100. package/dist/types/schemas/admin.js +0 -38
  101. package/dist/types/schemas/badge.d.ts +0 -37
  102. package/dist/types/schemas/badge.js +0 -11
  103. package/dist/types/schemas/campaign.d.ts +64 -832
  104. package/dist/types/schemas/campaign.js +2 -25
  105. package/dist/types/schemas/common.d.ts +5 -0
  106. package/dist/types/schemas/common.js +5 -0
  107. package/dist/types/schemas/export.d.ts +0 -17
  108. package/dist/types/schemas/export.js +0 -7
  109. package/dist/types/schemas/member.d.ts +37 -176
  110. package/dist/types/schemas/member.js +0 -27
  111. package/dist/types/schemas/points.d.ts +0 -63
  112. package/dist/types/schemas/points.js +0 -22
  113. package/dist/types/schemas/reward.d.ts +71 -68
  114. package/dist/types/schemas/reward.js +8 -28
  115. package/dist/types/schemas/role.d.ts +0 -100
  116. package/dist/types/schemas/role.js +0 -29
  117. package/dist/types/schemas/segment.d.ts +0 -58
  118. package/dist/types/schemas/segment.js +0 -17
  119. package/dist/types/schemas/tierset.d.ts +0 -176
  120. package/dist/types/schemas/tierset.js +0 -27
  121. package/dist/types/schemas/transaction.d.ts +23 -254
  122. package/dist/types/schemas/transaction.js +0 -7
  123. package/dist/types/schemas/wallet-type.d.ts +8 -8
  124. package/dist/types/schemas/wallet-type.js +1 -1
  125. package/dist/types/schemas/webhook.d.ts +0 -58
  126. package/dist/types/schemas/webhook.js +0 -12
  127. package/dist/utils/errors.js +30 -3
  128. package/dist/utils/payload.d.ts +12 -0
  129. package/dist/utils/payload.js +14 -0
  130. package/dist/workflows/app-login-streak.d.ts +39 -0
  131. package/dist/workflows/app-login-streak.js +298 -0
  132. package/dist/workflows/early-arrival.d.ts +33 -0
  133. package/dist/workflows/early-arrival.js +148 -0
  134. package/dist/workflows/index.d.ts +101 -0
  135. package/dist/workflows/index.js +208 -0
  136. package/dist/workflows/match-attendance.d.ts +45 -0
  137. package/dist/workflows/match-attendance.js +308 -0
  138. package/dist/workflows/sportsbar-visit.d.ts +41 -0
  139. package/dist/workflows/sportsbar-visit.js +284 -0
  140. package/dist/workflows/vod-watching.d.ts +43 -0
  141. package/dist/workflows/vod-watching.js +326 -0
  142. package/package.json +10 -2
@@ -0,0 +1,326 @@
1
+ /**
2
+ * VOD Watching Workflow
3
+ *
4
+ * Creates a campaign to reward fans for watching video content,
5
+ * with achievements for total watch time using attribute aggregation.
6
+ */
7
+ import { campaignCreate, campaignList } from "../tools/campaign/handlers.js";
8
+ import { achievementCreate, achievementList } from "../tools/achievement.js";
9
+ import { badgeList } from "../tools/badge.js";
10
+ import { formatOLDate, DEFAULTS } from "../prompts/fan-engagement-setup.js";
11
+ // ============================================================================
12
+ // Workflow Implementation
13
+ // ============================================================================
14
+ /**
15
+ * Execute the VOD watching workflow
16
+ */
17
+ export async function executeVodWatchingWorkflow(config = {}) {
18
+ const result = {
19
+ success: false,
20
+ errors: [],
21
+ summary: "",
22
+ };
23
+ // Merge with defaults
24
+ const cfg = {
25
+ trackBy: config.trackBy ?? DEFAULTS.vodWatching.trackBy,
26
+ coinsPerUnit: config.coinsPerUnit ?? DEFAULTS.vodWatching.coinsPerUnit,
27
+ unitSize: config.unitSize ?? DEFAULTS.vodWatching.unitSize,
28
+ createAchievement: config.createAchievement ?? true,
29
+ achievementTarget: config.achievementTarget ?? DEFAULTS.vodWatching.achievementMinutes,
30
+ achievementBonus: config.achievementBonus ?? DEFAULTS.vodWatching.achievementBonus,
31
+ badgeName: config.badgeName ?? "Content Enthusiast",
32
+ seasonStart: config.seasonStart ?? DEFAULTS.seasonDates.start,
33
+ seasonEnd: config.seasonEnd ?? DEFAULTS.seasonDates.end,
34
+ };
35
+ try {
36
+ // Step 1: Create base VOD watching campaign
37
+ const watchCampaignResult = await createWatchCampaign(cfg);
38
+ if (watchCampaignResult.error) {
39
+ result.errors.push(watchCampaignResult.error);
40
+ }
41
+ else {
42
+ result.watchCampaignId = watchCampaignResult.campaignId;
43
+ }
44
+ // Step 2: Create achievement and bonus campaign if enabled
45
+ if (cfg.createAchievement) {
46
+ const badges = await getAvailableBadges();
47
+ const achievementResult = await createWatchAchievement(cfg, badges);
48
+ if (achievementResult.error) {
49
+ result.errors.push(achievementResult.error);
50
+ }
51
+ else {
52
+ result.achievementId = achievementResult.achievementId;
53
+ // Create bonus campaign for achievement
54
+ if (achievementResult.achievementId) {
55
+ const bonusCampaignResult = await createAchievementBonusCampaign(achievementResult.achievementId, cfg);
56
+ if (bonusCampaignResult.error) {
57
+ result.errors.push(bonusCampaignResult.error);
58
+ }
59
+ else {
60
+ result.bonusCampaignId = bonusCampaignResult.campaignId;
61
+ }
62
+ }
63
+ }
64
+ }
65
+ // Step 3: Verify setup
66
+ const verification = await verifySetup(result);
67
+ if (verification.warnings.length > 0) {
68
+ result.errors.push(...verification.warnings);
69
+ }
70
+ // Determine success
71
+ result.success = result.watchCampaignId !== undefined && result.errors.length === 0;
72
+ // Build summary
73
+ result.summary = buildSummary(cfg, result);
74
+ }
75
+ catch (error) {
76
+ result.errors.push(`Workflow error: ${error instanceof Error ? error.message : String(error)}`);
77
+ }
78
+ return result;
79
+ }
80
+ // ============================================================================
81
+ // Helper Functions
82
+ // ============================================================================
83
+ async function createWatchCampaign(cfg) {
84
+ try {
85
+ const isMinutesBased = cfg.trackBy === "minutes";
86
+ // For minutes-based tracking, we use an expression to calculate points
87
+ // based on the minutes_watched attribute divided by unitSize
88
+ const pointsRule = isMinutesBased
89
+ ? {
90
+ expression: `Math.floor(event.minutes_watched / ${cfg.unitSize}) * ${cfg.coinsPerUnit}`,
91
+ }
92
+ : {
93
+ fixedValue: cfg.coinsPerUnit,
94
+ };
95
+ const description = isMinutesBased
96
+ ? `Earn ${cfg.coinsPerUnit} coins for every ${cfg.unitSize} minutes of video watched`
97
+ : `Earn ${cfg.coinsPerUnit} coins for each video viewed`;
98
+ const response = await campaignCreate({
99
+ type: "direct",
100
+ trigger: "custom_event",
101
+ event: "vod_watch",
102
+ translations: {
103
+ en: {
104
+ name: "Video Content Reward",
105
+ description,
106
+ },
107
+ },
108
+ activity: {
109
+ startsAt: formatOLDate(cfg.seasonStart),
110
+ endsAt: formatOLDate(cfg.seasonEnd),
111
+ },
112
+ rules: [
113
+ {
114
+ name: "Award coins for watching content",
115
+ effects: [
116
+ {
117
+ effect: "give_points",
118
+ pointsRule,
119
+ },
120
+ ],
121
+ },
122
+ ],
123
+ active: true,
124
+ });
125
+ return { campaignId: response.campaignId };
126
+ }
127
+ catch (error) {
128
+ return {
129
+ error: `Failed to create VOD watch campaign: ${error instanceof Error ? error.message : String(error)}`,
130
+ };
131
+ }
132
+ }
133
+ async function getAvailableBadges() {
134
+ try {
135
+ const response = await badgeList({});
136
+ const badgeMap = new Map();
137
+ for (const badge of response.badges) {
138
+ if (badge.name && badge.badgeTypeId) {
139
+ badgeMap.set(badge.name.toLowerCase(), badge.badgeTypeId);
140
+ }
141
+ }
142
+ return badgeMap;
143
+ }
144
+ catch {
145
+ return new Map();
146
+ }
147
+ }
148
+ async function createWatchAchievement(cfg, badges) {
149
+ try {
150
+ const badgeTypeId = cfg.badgeName
151
+ ? badges.get(cfg.badgeName.toLowerCase())
152
+ : undefined;
153
+ const isMinutesBased = cfg.trackBy === "minutes";
154
+ const targetLabel = isMinutesBased ? "minutes" : "videos";
155
+ /**
156
+ * For attribute-sum achievements, we use aggregation rule "sum" with
157
+ * the attribute to aggregate (e.g., "minutes_watched").
158
+ * The periodGoal is the total to reach.
159
+ */
160
+ const achievementPayload = {
161
+ translations: {
162
+ en: {
163
+ name: `Watch ${cfg.achievementTarget} ${targetLabel}`,
164
+ description: isMinutesBased
165
+ ? `Watch ${cfg.achievementTarget} total minutes of video content`
166
+ : `Watch ${cfg.achievementTarget} videos`,
167
+ },
168
+ },
169
+ active: true,
170
+ activity: {
171
+ startsAt: formatOLDate(cfg.seasonStart),
172
+ endsAt: formatOLDate(cfg.seasonEnd),
173
+ },
174
+ rules: [
175
+ {
176
+ trigger: "custom_event",
177
+ event: "vod_watch",
178
+ completeRule: {
179
+ periodGoal: cfg.achievementTarget,
180
+ },
181
+ // For minutes tracking, aggregate the minutes_watched attribute
182
+ ...(isMinutesBased && {
183
+ aggregation: {
184
+ rule: "sum",
185
+ },
186
+ }),
187
+ },
188
+ ],
189
+ };
190
+ if (badgeTypeId) {
191
+ achievementPayload.badgeTypeId = badgeTypeId;
192
+ }
193
+ const response = await achievementCreate(achievementPayload);
194
+ return { achievementId: response.achievementId };
195
+ }
196
+ catch (error) {
197
+ return {
198
+ error: `Failed to create watch achievement: ${error instanceof Error ? error.message : String(error)}`,
199
+ };
200
+ }
201
+ }
202
+ async function createAchievementBonusCampaign(achievementId, cfg) {
203
+ try {
204
+ const targetLabel = cfg.trackBy === "minutes" ? "minutes" : "videos";
205
+ const response = await campaignCreate({
206
+ type: "direct",
207
+ trigger: "achievement",
208
+ translations: {
209
+ en: {
210
+ name: `Watch ${cfg.achievementTarget} ${targetLabel} Bonus`,
211
+ description: `Bonus ${cfg.achievementBonus} coins for watching ${cfg.achievementTarget} ${targetLabel}`,
212
+ },
213
+ },
214
+ activity: {
215
+ startsAt: formatOLDate(cfg.seasonStart),
216
+ endsAt: formatOLDate(cfg.seasonEnd),
217
+ },
218
+ rules: [
219
+ {
220
+ name: `Bonus for watching ${cfg.achievementTarget} ${targetLabel}`,
221
+ effects: [
222
+ {
223
+ effect: "give_points",
224
+ pointsRule: { fixedValue: cfg.achievementBonus },
225
+ },
226
+ ],
227
+ conditions: [
228
+ {
229
+ operator: "is_equal",
230
+ attribute: "achievement.achievementId",
231
+ data: { value: achievementId },
232
+ },
233
+ ],
234
+ },
235
+ ],
236
+ active: true,
237
+ });
238
+ return { campaignId: response.campaignId };
239
+ }
240
+ catch (error) {
241
+ return {
242
+ error: `Failed to create bonus campaign: ${error instanceof Error ? error.message : String(error)}`,
243
+ };
244
+ }
245
+ }
246
+ async function verifySetup(result) {
247
+ const warnings = [];
248
+ try {
249
+ if (result.watchCampaignId) {
250
+ const campaigns = await campaignList({ active: true });
251
+ const found = campaigns.campaigns.some((c) => c.campaignId === result.watchCampaignId);
252
+ if (!found) {
253
+ warnings.push("Watch campaign created but not found in active campaigns list");
254
+ }
255
+ }
256
+ if (result.achievementId) {
257
+ const achievements = await achievementList({ active: true });
258
+ const found = achievements.achievements.some((a) => a.achievementId === result.achievementId);
259
+ if (!found) {
260
+ warnings.push("Achievement created but not found in active achievements list");
261
+ }
262
+ }
263
+ }
264
+ catch (error) {
265
+ warnings.push(`Verification error: ${error instanceof Error ? error.message : String(error)}`);
266
+ }
267
+ return { warnings };
268
+ }
269
+ function buildSummary(cfg, result) {
270
+ const lines = [];
271
+ const isMinutesBased = cfg.trackBy === "minutes";
272
+ const targetLabel = isMinutesBased ? "minutes" : "videos";
273
+ if (result.watchCampaignId) {
274
+ lines.push(`VOD watching campaign created!`);
275
+ lines.push(`\nTracking by: ${cfg.trackBy}`);
276
+ if (isMinutesBased) {
277
+ lines.push(`Reward: ${cfg.coinsPerUnit} coins per ${cfg.unitSize} minutes watched`);
278
+ }
279
+ else {
280
+ lines.push(`Reward: ${cfg.coinsPerUnit} coins per video viewed`);
281
+ }
282
+ }
283
+ if (result.achievementId) {
284
+ lines.push(`\nAchievement: Watch ${cfg.achievementTarget} ${targetLabel}`);
285
+ if (cfg.badgeName) {
286
+ lines.push(`Badge: ${cfg.badgeName}`);
287
+ }
288
+ if (result.bonusCampaignId) {
289
+ lines.push(`Completion bonus: ${cfg.achievementBonus} coins`);
290
+ }
291
+ }
292
+ lines.push(`\nSeason: ${cfg.seasonStart} to ${cfg.seasonEnd}`);
293
+ lines.push(`\nCustom event to trigger reward:`);
294
+ lines.push(` Event: vod_watch`);
295
+ if (isMinutesBased) {
296
+ lines.push(` Required attribute: minutes_watched (number)`);
297
+ lines.push(`\nExample event payload:`);
298
+ lines.push(`{`);
299
+ lines.push(` "event": "vod_watch",`);
300
+ lines.push(` "attributes": {`);
301
+ lines.push(` "minutes_watched": 15`);
302
+ lines.push(` }`);
303
+ lines.push(`}`);
304
+ }
305
+ else {
306
+ lines.push(`\nExample event payload:`);
307
+ lines.push(`{`);
308
+ lines.push(` "event": "vod_watch"`);
309
+ lines.push(`}`);
310
+ }
311
+ if (result.errors.length > 0) {
312
+ lines.push(`\nWarnings/Errors:`);
313
+ for (const error of result.errors) {
314
+ lines.push(`- ${error}`);
315
+ }
316
+ }
317
+ return lines.join("\n");
318
+ }
319
+ // ============================================================================
320
+ // Exports
321
+ // ============================================================================
322
+ export const vodWatchingWorkflow = {
323
+ id: "vod-watching",
324
+ name: "VOD Watching Campaign",
325
+ execute: executeVodWatchingWorkflow,
326
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-loyalty/mcp-server",
3
- "version": "1.0.3",
3
+ "version": "1.3.1",
4
4
  "type": "module",
5
5
  "description": "MCP server for Open Loyalty API - enables AI agents to manage loyalty programs, members, points, rewards, and transactions",
6
6
  "author": "Marcin Dyguda <md@openloyalty.io>",
@@ -36,7 +36,9 @@
36
36
  "test": "vitest",
37
37
  "test:run": "vitest run",
38
38
  "test:coverage": "vitest run --coverage",
39
- "test:watch": "vitest --watch"
39
+ "test:watch": "vitest --watch",
40
+ "lint": "eslint src/",
41
+ "lint:fix": "eslint src/ --fix"
40
42
  },
41
43
  "dependencies": {
42
44
  "@modelcontextprotocol/sdk": "^1.0.0",
@@ -44,17 +46,23 @@
44
46
  "cors": "^2.8.5",
45
47
  "dotenv": "^17.2.3",
46
48
  "express": "^5.2.1",
49
+ "express-rate-limit": "^8.2.1",
50
+ "form-data": "^4.0.0",
51
+ "helmet": "^8.1.0",
47
52
  "ioredis": "^5.9.2",
48
53
  "zod": "^3.22.0"
49
54
  },
50
55
  "devDependencies": {
56
+ "@eslint/js": "^9.39.2",
51
57
  "@types/cors": "^2.8.19",
52
58
  "@types/express": "^5.0.6",
53
59
  "@types/node": "^20.10.0",
54
60
  "@vitest/coverage-v8": "^4.0.17",
55
61
  "axios-mock-adapter": "^2.1.0",
62
+ "eslint": "^9.39.2",
56
63
  "tsx": "^4.7.0",
57
64
  "typescript": "^5.3.0",
65
+ "typescript-eslint": "^8.53.1",
58
66
  "vitest": "^4.0.17"
59
67
  },
60
68
  "engines": {