@open-loyalty/mcp-server 1.5.3 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/dist/config.d.ts +4 -0
  2. package/dist/config.js +11 -0
  3. package/dist/index.js +0 -8
  4. package/dist/server.js +13 -0
  5. package/dist/tools/achievement/handlers.js +47 -0
  6. package/dist/tools/achievement/index.d.ts +11 -4
  7. package/dist/tools/achievement/index.js +12 -1
  8. package/dist/tools/achievement/schemas.d.ts +4 -4
  9. package/dist/tools/achievement/schemas.js +13 -12
  10. package/dist/tools/admin/handlers.d.ts +48 -0
  11. package/dist/tools/admin/handlers.js +159 -0
  12. package/dist/tools/admin/index.d.ts +86 -0
  13. package/dist/tools/admin/index.js +64 -0
  14. package/dist/tools/admin/schemas.d.ts +40 -0
  15. package/dist/tools/admin/schemas.js +40 -0
  16. package/dist/tools/analytics/handlers.d.ts +42 -0
  17. package/dist/tools/analytics/handlers.js +282 -0
  18. package/dist/tools/analytics/index.d.ts +108 -0
  19. package/dist/tools/analytics/index.js +91 -0
  20. package/dist/tools/analytics/schemas.d.ts +42 -0
  21. package/dist/tools/analytics/schemas.js +47 -0
  22. package/dist/tools/apikey/handlers.d.ts +15 -0
  23. package/dist/tools/apikey/handlers.js +53 -0
  24. package/dist/tools/apikey/index.d.ts +41 -0
  25. package/dist/tools/apikey/index.js +38 -0
  26. package/dist/tools/apikey/schemas.d.ts +31 -0
  27. package/dist/tools/apikey/schemas.js +15 -0
  28. package/dist/tools/audit/handlers.d.ts +20 -0
  29. package/dist/tools/audit/handlers.js +82 -0
  30. package/dist/tools/audit/index.d.ts +36 -0
  31. package/dist/tools/audit/index.js +28 -0
  32. package/dist/tools/audit/schemas.d.ts +62 -0
  33. package/dist/tools/audit/schemas.js +18 -0
  34. package/dist/tools/badge/handlers.d.ts +45 -0
  35. package/dist/tools/badge/handlers.js +135 -0
  36. package/dist/tools/badge/index.d.ts +68 -0
  37. package/dist/tools/badge/index.js +47 -0
  38. package/dist/tools/badge/schemas.d.ts +37 -0
  39. package/dist/tools/badge/schemas.js +31 -0
  40. package/dist/tools/campaign/handlers.js +61 -0
  41. package/dist/tools/campaign/index.d.ts +12 -0
  42. package/dist/tools/campaign/index.js +20 -1
  43. package/dist/tools/campaign/member-handlers.js +37 -1
  44. package/dist/tools/campaign/schemas.js +16 -14
  45. package/dist/tools/custom-event/handlers.d.ts +98 -0
  46. package/dist/tools/custom-event/handlers.js +238 -0
  47. package/dist/tools/custom-event/index.d.ts +139 -0
  48. package/dist/tools/custom-event/index.js +78 -0
  49. package/dist/tools/custom-event/schemas.d.ts +87 -0
  50. package/dist/tools/custom-event/schemas.js +59 -0
  51. package/dist/tools/export/handlers.d.ts +29 -0
  52. package/dist/tools/export/handlers.js +128 -0
  53. package/dist/tools/export/index.d.ts +56 -0
  54. package/dist/tools/export/index.js +46 -0
  55. package/dist/tools/export/schemas.d.ts +42 -0
  56. package/dist/tools/export/schemas.js +41 -0
  57. package/dist/tools/import/handlers.d.ts +22 -0
  58. package/dist/tools/import/handlers.js +123 -0
  59. package/dist/tools/import/index.d.ts +45 -0
  60. package/dist/tools/import/index.js +41 -0
  61. package/dist/tools/import/schemas.d.ts +57 -0
  62. package/dist/tools/import/schemas.js +39 -0
  63. package/dist/tools/index.d.ts +1 -0
  64. package/dist/tools/index.js +11 -11
  65. package/dist/tools/member/handlers.js +30 -0
  66. package/dist/tools/member/index.d.ts +10 -0
  67. package/dist/tools/member/index.js +10 -0
  68. package/dist/tools/member/schemas.js +13 -13
  69. package/dist/tools/points/handlers.js +73 -0
  70. package/dist/tools/points/index.d.ts +6 -0
  71. package/dist/tools/points/index.js +6 -0
  72. package/dist/tools/points/schemas.js +1 -1
  73. package/dist/tools/referral/index.d.ts +3 -0
  74. package/dist/tools/referral/index.js +3 -0
  75. package/dist/tools/reward/handlers.js +21 -13
  76. package/dist/tools/reward/index.d.ts +9 -0
  77. package/dist/tools/reward/index.js +12 -1
  78. package/dist/tools/reward/schemas.js +2 -2
  79. package/dist/tools/role/handlers.d.ts +35 -0
  80. package/dist/tools/role/handlers.js +127 -0
  81. package/dist/tools/role/index.d.ts +99 -0
  82. package/dist/tools/role/index.js +65 -0
  83. package/dist/tools/role/schemas.d.ts +56 -0
  84. package/dist/tools/role/schemas.js +35 -0
  85. package/dist/tools/segment/handlers.js +68 -1
  86. package/dist/tools/segment/index.d.ts +9 -0
  87. package/dist/tools/segment/index.js +13 -0
  88. package/dist/tools/segment/schemas.js +8 -5
  89. package/dist/tools/store/handlers.d.ts +25 -0
  90. package/dist/tools/store/handlers.js +89 -0
  91. package/dist/tools/store/index.d.ts +55 -0
  92. package/dist/tools/store/index.js +46 -0
  93. package/dist/tools/store/schemas.d.ts +38 -0
  94. package/dist/tools/store/schemas.js +23 -0
  95. package/dist/tools/tierset/handlers.js +92 -1
  96. package/dist/tools/tierset/index.d.ts +6 -0
  97. package/dist/tools/tierset/index.js +8 -1
  98. package/dist/tools/transaction/handlers.js +40 -0
  99. package/dist/tools/transaction/index.d.ts +4 -0
  100. package/dist/tools/transaction/index.js +4 -0
  101. package/dist/tools/transaction/schemas.js +3 -3
  102. package/dist/tools/wallet-type/index.d.ts +4 -0
  103. package/dist/tools/wallet-type/index.js +5 -1
  104. package/dist/tools/webhook/handlers.d.ts +34 -0
  105. package/dist/tools/webhook/handlers.js +147 -0
  106. package/dist/tools/webhook/index.d.ts +97 -0
  107. package/dist/tools/webhook/index.js +65 -0
  108. package/dist/tools/webhook/schemas.d.ts +72 -0
  109. package/dist/tools/{webhook.js → webhook/schemas.js} +0 -140
  110. package/dist/types/schemas/tierset.js +3 -1
  111. package/package.json +1 -1
  112. package/dist/tools/admin.d.ts +0 -165
  113. package/dist/tools/admin.js +0 -205
  114. package/dist/tools/analytics.d.ts +0 -180
  115. package/dist/tools/analytics.js +0 -255
  116. package/dist/tools/apikey.d.ts +0 -79
  117. package/dist/tools/apikey.js +0 -85
  118. package/dist/tools/audit.d.ts +0 -111
  119. package/dist/tools/audit.js +0 -94
  120. package/dist/tools/badge.d.ts +0 -143
  121. package/dist/tools/badge.js +0 -174
  122. package/dist/tools/custom-event.d.ts +0 -315
  123. package/dist/tools/custom-event.js +0 -271
  124. package/dist/tools/export.d.ts +0 -118
  125. package/dist/tools/export.js +0 -152
  126. package/dist/tools/import.d.ts +0 -116
  127. package/dist/tools/import.js +0 -143
  128. package/dist/tools/role.d.ts +0 -180
  129. package/dist/tools/role.js +0 -173
  130. package/dist/tools/store.d.ts +0 -109
  131. package/dist/tools/store.js +0 -125
  132. package/dist/tools/webhook.d.ts +0 -192
package/dist/config.d.ts CHANGED
@@ -27,5 +27,9 @@ export declare function runWithConfig<T>(override: {
27
27
  * Throws a clear error if no store code is available from either source.
28
28
  */
29
29
  export declare function getStoreCode(storeCode?: string): string;
30
+ /**
31
+ * Returns true if required env vars are set (non-throwing check).
32
+ */
33
+ export declare function isConfigured(): boolean;
30
34
  export declare function getConfig(): Config;
31
35
  export {};
package/dist/config.js CHANGED
@@ -32,6 +32,17 @@ export function getStoreCode(storeCode) {
32
32
  }
33
33
  return code;
34
34
  }
35
+ /**
36
+ * Returns true if required env vars are set (non-throwing check).
37
+ */
38
+ export function isConfigured() {
39
+ if (config)
40
+ return true;
41
+ const requestConfig = configStorage.getStore();
42
+ if (requestConfig)
43
+ return true;
44
+ return !!(process.env.OPENLOYALTY_API_URL && process.env.OPENLOYALTY_API_TOKEN);
45
+ }
35
46
  export function getConfig() {
36
47
  // Return request-scoped config if set (OAuth mode)
37
48
  const requestConfig = configStorage.getStore();
package/dist/index.js CHANGED
@@ -2,15 +2,7 @@
2
2
  import "dotenv/config";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { createServer } from "./server.js";
5
- import { getConfig } from "./config.js";
6
5
  async function main() {
7
- try {
8
- getConfig();
9
- }
10
- catch (error) {
11
- console.error("Configuration error:", error instanceof Error ? error.message : error);
12
- process.exit(1);
13
- }
14
6
  const server = createServer();
15
7
  const transport = new StdioServerTransport();
16
8
  await server.connect(transport);
package/dist/server.js CHANGED
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { getAllTools, getToolHandler } from "./tools/index.js";
4
4
  import { OpenLoyaltyError } from "./utils/errors.js";
5
5
  import { SERVER_INSTRUCTIONS } from "./instructions.js";
6
+ import { isConfigured } from "./config.js";
6
7
  /**
7
8
  * Convert an inputSchema object (Record<string, ZodType>) to a Zod object schema.
8
9
  * This allows centralized validation of all tool inputs.
@@ -50,9 +51,21 @@ export function createServer() {
50
51
  annotations: {
51
52
  readOnlyHint: tool.readOnly,
52
53
  destructiveHint: tool.destructive,
54
+ idempotentHint: tool.idempotent,
53
55
  openWorldHint: true,
54
56
  },
55
57
  }, async (args) => {
58
+ if (!isConfigured()) {
59
+ return {
60
+ content: [
61
+ {
62
+ type: "text",
63
+ text: "Open Loyalty MCP server is not configured yet.\n\nRequired environment variables:\n- OPENLOYALTY_API_URL: Your OL instance API endpoint (e.g. https://your-instance.openloyalty.io/api)\n- OPENLOYALTY_API_TOKEN: API authentication token (Admin Panel > Settings > API Keys)\n- OPENLOYALTY_DEFAULT_STORE_CODE: Store identifier (optional, defaults to 'default')\n\nRun /openloyalty:setup in Claude Code to configure.",
64
+ },
65
+ ],
66
+ isError: true,
67
+ };
68
+ }
56
69
  const handler = getToolHandler(tool.name);
57
70
  if (!handler) {
58
71
  return {
@@ -125,6 +125,15 @@ export async function achievementGet(input) {
125
125
  return response;
126
126
  }
127
127
  catch (error) {
128
+ const axiosError = error;
129
+ if (axiosError.response?.status === 404) {
130
+ throw new OpenLoyaltyError({
131
+ code: "NOT_FOUND",
132
+ message: `Achievement '${input.achievementId}' not found`,
133
+ hint: "Use ol_achievement_list() to find existing achievements and their IDs.",
134
+ relatedTool: "ol_achievement_get",
135
+ });
136
+ }
128
137
  throw formatApiError(error, "ol_achievement_get");
129
138
  }
130
139
  }
@@ -147,6 +156,29 @@ export async function achievementUpdate(input) {
147
156
  await apiPut(`/${storeCode}/achievement/${input.achievementId}`, { achievement: achievementPayload });
148
157
  }
149
158
  catch (error) {
159
+ const axiosError = error;
160
+ if (axiosError.response?.status === 404) {
161
+ throw new OpenLoyaltyError({ code: "NOT_FOUND", message: `Achievement '${input.achievementId}' not found`,
162
+ hint: "Use ol_achievement_list() to find existing achievements and their IDs.", relatedTool: "ol_achievement_update" });
163
+ }
164
+ const apiErrors = axiosError.response?.data?.errors || [];
165
+ const allMessages = [error instanceof Error ? error.message : "", axiosError.response?.data?.message || "",
166
+ ...apiErrors.map(e => `${e.path || ""}: ${e.message}`)].join(" ").toLowerCase();
167
+ if (allMessages.includes("period") && (allMessages.includes("choice is invalid") || allMessages.includes("not valid"))) {
168
+ throw new OpenLoyaltyError({ code: "INVALID_PERIOD_TYPE", message: "Invalid period type in achievement rules",
169
+ hint: "For period.type use: 'day' (NOT 'days'), 'week', 'month', 'year', 'calendarDays', 'calendarWeeks', 'calendarMonths', 'calendarYears'.",
170
+ relatedTool: "ol_achievement_update" });
171
+ }
172
+ if (allMessages.includes("interval") && (allMessages.includes("choice is invalid") || allMessages.includes("not valid"))) {
173
+ throw new OpenLoyaltyError({ code: "INVALID_INTERVAL_TYPE", message: "Invalid interval type in achievement rule limit",
174
+ hint: "For limit.interval.type use: 'calendarDays' (NOT 'days'), 'calendarWeeks', 'calendarMonths', 'calendarYears'.",
175
+ relatedTool: "ol_achievement_update" });
176
+ }
177
+ if (allMessages.includes("choice is invalid") || allMessages.includes("selected choice is invalid")) {
178
+ throw new OpenLoyaltyError({ code: "INVALID_ENUM_VALUE", message: "An enum value in the achievement configuration is invalid",
179
+ hint: "Check: period.type ('day' not 'days'), limit.interval.type ('calendarDays' not 'days'), trigger, aggregation.type ('quantity').",
180
+ relatedTool: "ol_achievement_update" });
181
+ }
150
182
  throw formatApiError(error, "ol_achievement_update");
151
183
  }
152
184
  }
@@ -162,6 +194,11 @@ export async function achievementPatch(input) {
162
194
  await apiPatch(`/${storeCode}/achievement/${input.achievementId}`, { achievement: achievementPayload });
163
195
  }
164
196
  catch (error) {
197
+ const axiosError = error;
198
+ if (axiosError.response?.status === 404) {
199
+ throw new OpenLoyaltyError({ code: "NOT_FOUND", message: `Achievement '${input.achievementId}' not found`,
200
+ hint: "Use ol_achievement_list() to find existing achievements and their IDs.", relatedTool: "ol_achievement_patch" });
201
+ }
165
202
  throw formatApiError(error, "ol_achievement_patch");
166
203
  }
167
204
  }
@@ -186,6 +223,11 @@ export async function achievementGetMemberProgress(input) {
186
223
  return achievements[0];
187
224
  }
188
225
  catch (error) {
226
+ const axiosError = error;
227
+ if (axiosError.response?.status === 404) {
228
+ throw new OpenLoyaltyError({ code: "NOT_FOUND", message: `Member '${input.memberId}' not found`,
229
+ hint: "Use ol_member_list() to search for the member by email, name, or phone.", relatedTool: "ol_achievement_get_member_progress" });
230
+ }
189
231
  throw formatApiError(error, "ol_achievement_get_member_progress");
190
232
  }
191
233
  }
@@ -215,6 +257,11 @@ export async function achievementListMemberAchievements(input) {
215
257
  };
216
258
  }
217
259
  catch (error) {
260
+ const axiosError = error;
261
+ if (axiosError.response?.status === 404) {
262
+ throw new OpenLoyaltyError({ code: "NOT_FOUND", message: `Member '${input.memberId}' not found`,
263
+ hint: "Use ol_member_list() to search for the member by email, name, or phone.", relatedTool: "ol_achievement_list_member_achievements" });
264
+ }
218
265
  throw formatApiError(error, "ol_achievement_list_member_achievements");
219
266
  }
220
267
  }
@@ -7,6 +7,7 @@ export declare const achievementToolDefinitions: readonly [{
7
7
  readonly title: "List Achievements";
8
8
  readonly description: "List achievements. Achievements gamify member behavior by setting goals (e.g., 'Make 5 purchases this month'). Returns achievementId, name, active status, and associated badge. Use achievement_get for full rules and configuration.";
9
9
  readonly readOnly: true;
10
+ readonly idempotent: true;
10
11
  readonly inputSchema: {
11
12
  storeCode: import("zod").ZodOptional<import("zod").ZodString>;
12
13
  page: import("zod").ZodOptional<import("zod").ZodNumber>;
@@ -20,6 +21,7 @@ export declare const achievementToolDefinitions: readonly [{
20
21
  readonly title: "Create Achievement";
21
22
  readonly description: string;
22
23
  readonly readOnly: false;
24
+ readonly idempotent: false;
23
25
  readonly inputSchema: {
24
26
  storeCode: import("zod").ZodOptional<import("zod").ZodString>;
25
27
  translations: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodObject<{
@@ -155,7 +157,7 @@ export declare const achievementToolDefinitions: readonly [{
155
157
  uniqueReferee: import("zod").ZodOptional<import("zod").ZodBoolean>;
156
158
  }, "strip", import("zod").ZodTypeAny, {
157
159
  type: "direct" | "referral";
158
- trigger: "referral" | "transaction" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
160
+ trigger: "transaction" | "referral" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
159
161
  completeRule: {
160
162
  periodGoal: string | number;
161
163
  period: {
@@ -185,7 +187,7 @@ export declare const achievementToolDefinitions: readonly [{
185
187
  uniqueReferee?: boolean | undefined;
186
188
  }, {
187
189
  type: "direct" | "referral";
188
- trigger: "referral" | "transaction" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
190
+ trigger: "transaction" | "referral" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
189
191
  completeRule: {
190
192
  periodGoal: string | number;
191
193
  period: {
@@ -222,6 +224,7 @@ export declare const achievementToolDefinitions: readonly [{
222
224
  readonly title: "Get Achievement Details";
223
225
  readonly description: "Get achievement details including all rules, conditions, activity period, limits, and completions count.";
224
226
  readonly readOnly: true;
227
+ readonly idempotent: true;
225
228
  readonly inputSchema: {
226
229
  storeCode: import("zod").ZodOptional<import("zod").ZodString>;
227
230
  achievementId: import("zod").ZodString;
@@ -232,6 +235,7 @@ export declare const achievementToolDefinitions: readonly [{
232
235
  readonly title: "Update Achievement";
233
236
  readonly description: "Update achievement configuration. Requires full achievement object (translations, rules). Use achievement_get first to retrieve current configuration.";
234
237
  readonly readOnly: false;
238
+ readonly idempotent: true;
235
239
  readonly inputSchema: {
236
240
  storeCode: import("zod").ZodOptional<import("zod").ZodString>;
237
241
  achievementId: import("zod").ZodString;
@@ -368,7 +372,7 @@ export declare const achievementToolDefinitions: readonly [{
368
372
  uniqueReferee: import("zod").ZodOptional<import("zod").ZodBoolean>;
369
373
  }, "strip", import("zod").ZodTypeAny, {
370
374
  type: "direct" | "referral";
371
- trigger: "referral" | "transaction" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
375
+ trigger: "transaction" | "referral" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
372
376
  completeRule: {
373
377
  periodGoal: string | number;
374
378
  period: {
@@ -398,7 +402,7 @@ export declare const achievementToolDefinitions: readonly [{
398
402
  uniqueReferee?: boolean | undefined;
399
403
  }, {
400
404
  type: "direct" | "referral";
401
- trigger: "referral" | "transaction" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
405
+ trigger: "transaction" | "referral" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
402
406
  completeRule: {
403
407
  periodGoal: string | number;
404
408
  period: {
@@ -435,6 +439,7 @@ export declare const achievementToolDefinitions: readonly [{
435
439
  readonly title: "Patch Achievement";
436
440
  readonly description: "Partial update of achievement. Use for simple changes like activating/deactivating or updating translations without providing full rules.";
437
441
  readonly readOnly: false;
442
+ readonly idempotent: true;
438
443
  readonly inputSchema: {
439
444
  storeCode: import("zod").ZodOptional<import("zod").ZodString>;
440
445
  achievementId: import("zod").ZodString;
@@ -456,6 +461,7 @@ export declare const achievementToolDefinitions: readonly [{
456
461
  readonly title: "Get Member Achievement Progress";
457
462
  readonly description: "Get member's progress on a specific achievement. Returns completedCount, and for each rule: periodGoal, currentPeriodValue, and consecutive period tracking.";
458
463
  readonly readOnly: true;
464
+ readonly idempotent: true;
459
465
  readonly inputSchema: {
460
466
  storeCode: import("zod").ZodOptional<import("zod").ZodString>;
461
467
  memberId: import("zod").ZodString;
@@ -467,6 +473,7 @@ export declare const achievementToolDefinitions: readonly [{
467
473
  readonly title: "List Member Achievements";
468
474
  readonly description: "List all achievements with member's progress. Returns each achievement's status, completion count, and per-rule progress. Use for displaying gamification dashboard.";
469
475
  readonly readOnly: true;
476
+ readonly idempotent: true;
470
477
  readonly inputSchema: {
471
478
  storeCode: import("zod").ZodOptional<import("zod").ZodString>;
472
479
  memberId: import("zod").ZodString;
@@ -10,6 +10,7 @@ export const achievementToolDefinitions = [
10
10
  title: "List Achievements",
11
11
  description: "List achievements. Achievements gamify member behavior by setting goals (e.g., 'Make 5 purchases this month'). Returns achievementId, name, active status, and associated badge. Use achievement_get for full rules and configuration.",
12
12
  readOnly: true,
13
+ idempotent: true,
13
14
  inputSchema: AchievementListInputSchema,
14
15
  handler: achievementList,
15
16
  },
@@ -25,8 +26,13 @@ export const achievementToolDefinitions = [
25
26
  "PATTERNS: " +
26
27
  "ONE-TIME: period: { type: 'day', consecutive: 1 }, limit: { value: 1 }, NO rule-level limit. " +
27
28
  "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.",
29
+ "Every rule requires: type='direct', aggregation: { type: 'quantity' }, completeRule with periodGoal. " +
30
+ "COMPLETE EXAMPLE for one-time badge: " +
31
+ "rules: [{ type: 'direct', trigger: 'transaction', aggregation: { type: 'quantity' }, " +
32
+ "completeRule: { periodGoal: 1, period: { type: 'day', consecutive: 1 } } }], " +
33
+ "limit: { value: 1 }",
29
34
  readOnly: false,
35
+ idempotent: false,
30
36
  inputSchema: AchievementCreateInputSchema,
31
37
  handler: achievementCreate,
32
38
  },
@@ -35,6 +41,7 @@ export const achievementToolDefinitions = [
35
41
  title: "Get Achievement Details",
36
42
  description: "Get achievement details including all rules, conditions, activity period, limits, and completions count.",
37
43
  readOnly: true,
44
+ idempotent: true,
38
45
  inputSchema: AchievementGetInputSchema,
39
46
  handler: achievementGet,
40
47
  },
@@ -43,6 +50,7 @@ export const achievementToolDefinitions = [
43
50
  title: "Update Achievement",
44
51
  description: "Update achievement configuration. Requires full achievement object (translations, rules). Use achievement_get first to retrieve current configuration.",
45
52
  readOnly: false,
53
+ idempotent: true,
46
54
  inputSchema: AchievementUpdateInputSchema,
47
55
  handler: achievementUpdate,
48
56
  },
@@ -51,6 +59,7 @@ export const achievementToolDefinitions = [
51
59
  title: "Patch Achievement",
52
60
  description: "Partial update of achievement. Use for simple changes like activating/deactivating or updating translations without providing full rules.",
53
61
  readOnly: false,
62
+ idempotent: true,
54
63
  inputSchema: AchievementPatchInputSchema,
55
64
  handler: achievementPatch,
56
65
  },
@@ -59,6 +68,7 @@ export const achievementToolDefinitions = [
59
68
  title: "Get Member Achievement Progress",
60
69
  description: "Get member's progress on a specific achievement. Returns completedCount, and for each rule: periodGoal, currentPeriodValue, and consecutive period tracking.",
61
70
  readOnly: true,
71
+ idempotent: true,
62
72
  inputSchema: AchievementGetMemberProgressInputSchema,
63
73
  handler: achievementGetMemberProgress,
64
74
  },
@@ -67,6 +77,7 @@ export const achievementToolDefinitions = [
67
77
  title: "List Member Achievements",
68
78
  description: "List all achievements with member's progress. Returns each achievement's status, completion count, and per-rule progress. Use for displaying gamification dashboard.",
69
79
  readOnly: true,
80
+ idempotent: true,
70
81
  inputSchema: AchievementListMemberAchievementsInputSchema,
71
82
  handler: achievementListMemberAchievements,
72
83
  },
@@ -141,7 +141,7 @@ export declare const AchievementCreateInputSchema: {
141
141
  uniqueReferee: z.ZodOptional<z.ZodBoolean>;
142
142
  }, "strip", z.ZodTypeAny, {
143
143
  type: "direct" | "referral";
144
- trigger: "referral" | "transaction" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
144
+ trigger: "transaction" | "referral" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
145
145
  completeRule: {
146
146
  periodGoal: string | number;
147
147
  period: {
@@ -171,7 +171,7 @@ export declare const AchievementCreateInputSchema: {
171
171
  uniqueReferee?: boolean | undefined;
172
172
  }, {
173
173
  type: "direct" | "referral";
174
- trigger: "referral" | "transaction" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
174
+ trigger: "transaction" | "referral" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
175
175
  completeRule: {
176
176
  periodGoal: string | number;
177
177
  period: {
@@ -342,7 +342,7 @@ export declare const AchievementUpdateInputSchema: {
342
342
  uniqueReferee: z.ZodOptional<z.ZodBoolean>;
343
343
  }, "strip", z.ZodTypeAny, {
344
344
  type: "direct" | "referral";
345
- trigger: "referral" | "transaction" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
345
+ trigger: "transaction" | "referral" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
346
346
  completeRule: {
347
347
  periodGoal: string | number;
348
348
  period: {
@@ -372,7 +372,7 @@ export declare const AchievementUpdateInputSchema: {
372
372
  uniqueReferee?: boolean | undefined;
373
373
  }, {
374
374
  type: "direct" | "referral";
375
- trigger: "referral" | "transaction" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
375
+ trigger: "transaction" | "referral" | "custom_event" | "achievement" | "points_transfer" | "reward_redemption" | "tier_change" | "profile_update";
376
376
  completeRule: {
377
377
  periodGoal: string | number;
378
378
  period: {
@@ -83,15 +83,15 @@ export const AchievementCreateInputSchema = {
83
83
  })).describe("Achievement name and description. At least 'en' key required."),
84
84
  active: z.boolean().optional().describe("Whether achievement is active (default: false)."),
85
85
  activity: z.object({
86
- startsAt: z.string().optional().describe("ISO datetime when achievement becomes active."),
87
- endsAt: z.string().optional().describe("ISO datetime when achievement ends."),
88
- operator: z.string().optional().describe("Activity condition operator."),
86
+ startsAt: z.string().optional().describe("ISO 8601 datetime when achievement becomes active (e.g., '2026-01-01T00:00:00+00:00')."),
87
+ endsAt: z.string().optional().describe("ISO 8601 datetime when achievement ends (e.g., '2026-12-31T23:59:59+00:00')."),
88
+ operator: z.string().optional().describe("Activity condition operator: 'between' (both startsAt and endsAt), 'from' (start only), or 'to' (end only)."),
89
89
  }).optional().describe("Time period configuration."),
90
90
  limit: z.object({
91
- value: z.number().optional().describe("Maximum completions (e.g., 1 for one-time badges)."),
91
+ value: z.number().optional().describe("Maximum completions (e.g., 1 for one-time badges). Omit for unlimited."),
92
92
  interval: z.object({
93
- type: z.string().describe("Interval type. Valid values: 'calendarDays', 'calendarWeeks', 'calendarMonths', 'calendarYears'."),
94
- value: z.number().optional().describe("Interval length (e.g., 1 for every 1 calendar period)."),
93
+ type: z.string().describe("Interval type. Valid values: 'calendarDays', 'calendarWeeks', 'calendarMonths', 'calendarYears'. NOT 'days'/'weeks' -- must use 'calendar' prefix."),
94
+ value: z.number().optional().describe("Interval length (e.g., 1 for every 1 calendar period, 7 for every 7 calendar days)."),
95
95
  }).optional().describe("Interval for repeatable achievements (omit for one-time badges)."),
96
96
  }).optional().describe("How many times a member can earn this achievement. " +
97
97
  "BUSINESS DECISION: For one-time badges ('First Purchase'): set value=1 with NO interval. For repeatable challenges: omit or set higher. " +
@@ -115,18 +115,19 @@ export const AchievementUpdateInputSchema = {
115
115
  active: z.boolean().optional().describe("Whether achievement is active."),
116
116
  activity: z.object({
117
117
  startsAt: z.string().optional().describe("ISO datetime when achievement becomes active (e.g., '2026-01-01T00:00:00+00:00')."),
118
- endsAt: z.string().optional().describe("ISO datetime when achievement ends."),
119
- operator: z.string().optional().describe("Activity condition operator: 'between' (both dates) or 'from' (start only) or 'to' (end only)."),
118
+ endsAt: z.string().optional().describe("ISO datetime when achievement ends (e.g., '2026-12-31T23:59:59+00:00')."),
119
+ operator: z.string().optional().describe("Activity condition operator: 'between' (both startsAt and endsAt), 'from' (start only), or 'to' (end only)."),
120
120
  }).optional().describe("Time period configuration."),
121
121
  limit: z.object({
122
- value: z.number().optional().describe("Maximum completions (e.g., 1 for one-time badges)."),
122
+ value: z.number().optional().describe("Maximum completions (e.g., 1 for one-time badges). Omit for unlimited."),
123
123
  interval: z.object({
124
- type: z.string().describe("Interval type. Valid values: 'calendarDays', 'calendarWeeks', 'calendarMonths', 'calendarYears'."),
125
- value: z.number().optional().describe("Interval length (e.g., 1 for every 1 calendar period)."),
124
+ type: z.string().describe("Interval type. Valid values: 'calendarDays', 'calendarWeeks', 'calendarMonths', 'calendarYears'. NOT 'days'/'weeks' -- must use 'calendar' prefix."),
125
+ value: z.number().optional().describe("Interval length (e.g., 1 for every 1 calendar period, 7 for every 7 calendar days)."),
126
126
  }).optional().describe("Interval for repeatable achievements (omit for one-time badges)."),
127
127
  }).optional().describe("Achievement limit. For ONE-TIME badges: { value: 1 } with NO interval. " +
128
128
  "For repeatable: omit or set higher value."),
129
- rules: z.array(AchievementRuleInputSchema).describe("Achievement rules. Use achievement_get first to retrieve current rules and include achievementRuleId for existing rules."),
129
+ rules: z.array(AchievementRuleInputSchema).describe("Achievement rules defining completion criteria. Use achievement_get first to retrieve current rules and include achievementRuleId for existing rules. " +
130
+ "See AchievementCreateInputSchema rules for full pattern guidance (ONE-TIME vs STREAK)."),
130
131
  badgeTypeId: z.string().optional().describe("Badge type ID to award. Use ol_badge_list() to find valid IDs. " +
131
132
  "May cause 'extra fields' errors in some API versions - try without if creation fails."),
132
133
  };
@@ -0,0 +1,48 @@
1
+ import { AdminUser, AdminUserListItem, AdminPermission } from "../../types/schemas/admin.js";
2
+ export declare function adminList(input: {
3
+ page?: number;
4
+ perPage?: number;
5
+ email?: string;
6
+ isActive?: boolean;
7
+ firstName?: string;
8
+ lastName?: string;
9
+ role?: string;
10
+ }): Promise<{
11
+ items: AdminUserListItem[];
12
+ total?: number;
13
+ }>;
14
+ export declare function adminCreate(input: {
15
+ email: string;
16
+ password: string;
17
+ firstName?: string;
18
+ lastName?: string;
19
+ phone?: string;
20
+ roles?: (string | number)[];
21
+ isActive?: boolean;
22
+ external?: boolean;
23
+ notificationsEnabled?: boolean;
24
+ }): Promise<{
25
+ adminId: string;
26
+ }>;
27
+ export declare function adminGet(input: {
28
+ adminId: string;
29
+ }): Promise<AdminUser>;
30
+ export declare function adminUpdate(input: {
31
+ adminId: string;
32
+ email?: string;
33
+ firstName?: string;
34
+ lastName?: string;
35
+ phone?: string;
36
+ roles?: (string | number)[];
37
+ isActive?: boolean;
38
+ external?: boolean;
39
+ notificationsEnabled?: boolean;
40
+ password?: string;
41
+ }): Promise<{
42
+ adminId: string;
43
+ }>;
44
+ export declare function adminChangePassword(input: {
45
+ currentPassword: string;
46
+ newPassword: string;
47
+ }): Promise<void>;
48
+ export declare function adminGetPermissions(): Promise<AdminPermission>;
@@ -0,0 +1,159 @@
1
+ import { apiGet, apiPost, apiPut } from "../../client/http.js";
2
+ import { formatApiError, OpenLoyaltyError } from "../../utils/errors.js";
3
+ import axios from "axios";
4
+ export async function adminList(input) {
5
+ const params = new URLSearchParams();
6
+ if (input.page)
7
+ params.append("_page", String(input.page));
8
+ if (input.perPage)
9
+ params.append("_itemsOnPage", String(input.perPage));
10
+ if (input.email)
11
+ params.append("email", input.email);
12
+ if (input.isActive !== undefined)
13
+ params.append("isActive", String(input.isActive));
14
+ if (input.firstName)
15
+ params.append("firstName", input.firstName);
16
+ if (input.lastName)
17
+ params.append("lastName", input.lastName);
18
+ if (input.role)
19
+ params.append("role", input.role);
20
+ const queryString = params.toString();
21
+ const url = `/admin${queryString ? `?${queryString}` : ""}`;
22
+ try {
23
+ const response = await apiGet(url);
24
+ return response;
25
+ }
26
+ catch (error) {
27
+ if (axios.isAxiosError(error) && error.response?.status === 403) {
28
+ throw new OpenLoyaltyError({
29
+ code: "ADMIN_PERMISSION_DENIED",
30
+ message: "You don't have permission to list admin users",
31
+ hint: "Admin management requires the ADMIN:VIEW permission or super admin access. Use ol_admin_get_permissions() to check your current access level.",
32
+ relatedTool: "ol_admin_list",
33
+ });
34
+ }
35
+ throw formatApiError(error, "ol_admin_list");
36
+ }
37
+ }
38
+ export async function adminCreate(input) {
39
+ const payload = {
40
+ email: input.email,
41
+ plainPassword: input.password,
42
+ };
43
+ if (input.firstName)
44
+ payload.firstName = input.firstName;
45
+ if (input.lastName)
46
+ payload.lastName = input.lastName;
47
+ if (input.phone)
48
+ payload.phone = input.phone;
49
+ if (input.roles)
50
+ payload.roles = input.roles;
51
+ if (input.isActive !== undefined)
52
+ payload.isActive = input.isActive;
53
+ if (input.external !== undefined)
54
+ payload.external = input.external;
55
+ if (input.notificationsEnabled !== undefined)
56
+ payload.notificationsEnabled = input.notificationsEnabled;
57
+ try {
58
+ const response = await apiPost("/admin/data", { admin: payload });
59
+ return response;
60
+ }
61
+ catch (error) {
62
+ if (axios.isAxiosError(error)) {
63
+ const allMessages = [error.response?.data?.message || "", ...(error.response?.data?.errors || []).map((e) => e.message)].join(" ").toLowerCase();
64
+ if (allMessages.includes("email") && (allMessages.includes("already") || allMessages.includes("unique") || allMessages.includes("exists"))) {
65
+ throw new OpenLoyaltyError({ code: "DUPLICATE_EMAIL", message: `Admin with email '${input.email}' already exists`,
66
+ hint: `Use ol_admin_list(email: "${input.email}") to find the existing admin.`, relatedTool: "ol_admin_create" });
67
+ }
68
+ }
69
+ throw formatApiError(error, "ol_admin_create");
70
+ }
71
+ }
72
+ export async function adminGet(input) {
73
+ try {
74
+ const response = await apiGet(`/admin/data/${input.adminId}`);
75
+ return response;
76
+ }
77
+ catch (error) {
78
+ if (axios.isAxiosError(error) && error.response?.status === 404) {
79
+ throw new OpenLoyaltyError({ code: "NOT_FOUND", message: `Admin '${input.adminId}' not found`,
80
+ hint: "Use ol_admin_list() to find existing admin users and their IDs.", relatedTool: "ol_admin_get" });
81
+ }
82
+ throw formatApiError(error, "ol_admin_get");
83
+ }
84
+ }
85
+ export async function adminUpdate(input) {
86
+ const payload = {};
87
+ if (input.email)
88
+ payload.email = input.email;
89
+ if (input.firstName)
90
+ payload.firstName = input.firstName;
91
+ if (input.lastName)
92
+ payload.lastName = input.lastName;
93
+ if (input.phone)
94
+ payload.phone = input.phone;
95
+ if (input.roles)
96
+ payload.roles = input.roles;
97
+ if (input.isActive !== undefined)
98
+ payload.isActive = input.isActive;
99
+ if (input.external !== undefined)
100
+ payload.external = input.external;
101
+ if (input.notificationsEnabled !== undefined)
102
+ payload.notificationsEnabled = input.notificationsEnabled;
103
+ if (input.password)
104
+ payload.plainPassword = input.password;
105
+ try {
106
+ const response = await apiPut(`/admin/data/${input.adminId}`, { admin: payload });
107
+ return response;
108
+ }
109
+ catch (error) {
110
+ if (axios.isAxiosError(error) && error.response?.status === 404) {
111
+ throw new OpenLoyaltyError({ code: "NOT_FOUND", message: `Admin '${input.adminId}' not found`,
112
+ hint: "Use ol_admin_list() to find existing admin users and their IDs.", relatedTool: "ol_admin_update" });
113
+ }
114
+ throw formatApiError(error, "ol_admin_update");
115
+ }
116
+ }
117
+ export async function adminChangePassword(input) {
118
+ try {
119
+ await apiPut("/admin/password", {
120
+ currentPassword: input.currentPassword,
121
+ plainPassword: input.newPassword,
122
+ });
123
+ }
124
+ catch (error) {
125
+ if (axios.isAxiosError(error)) {
126
+ const allMessages = [error.response?.data?.message || "", ...(error.response?.data?.errors || []).map((e) => e.message)].join(" ").toLowerCase();
127
+ if (allMessages.includes("password") && (allMessages.includes("invalid") || allMessages.includes("incorrect") || allMessages.includes("wrong") || allMessages.includes("mismatch"))) {
128
+ throw new OpenLoyaltyError({ code: "INVALID_PASSWORD", message: "Current password is incorrect",
129
+ hint: "The currentPassword you provided does not match. Verify the current password and try again.", relatedTool: "ol_admin_change_password" });
130
+ }
131
+ if (allMessages.includes("password") && (allMessages.includes("short") || allMessages.includes("weak") || allMessages.includes("length") || allMessages.includes("requirements") || allMessages.includes("strong"))) {
132
+ throw new OpenLoyaltyError({
133
+ code: "WEAK_PASSWORD",
134
+ message: "The new password does not meet security requirements",
135
+ hint: "Password must be at least 8 characters and include a mix of uppercase, lowercase, numbers, and special characters.",
136
+ relatedTool: "ol_admin_change_password",
137
+ });
138
+ }
139
+ }
140
+ throw formatApiError(error, "ol_admin_change_password");
141
+ }
142
+ }
143
+ export async function adminGetPermissions() {
144
+ try {
145
+ const response = await apiGet("/admin/permissions");
146
+ return response;
147
+ }
148
+ catch (error) {
149
+ if (axios.isAxiosError(error) && error.response?.status === 401) {
150
+ throw new OpenLoyaltyError({
151
+ code: "UNAUTHORIZED",
152
+ message: "Cannot retrieve permissions - authentication failed",
153
+ hint: "Your API token may be invalid or expired. Check the OL_API_TOKEN environment variable. If using OAuth, the token may need to be refreshed.",
154
+ relatedTool: "ol_admin_get_permissions",
155
+ });
156
+ }
157
+ throw formatApiError(error, "ol_admin_get_permissions");
158
+ }
159
+ }