@open-loyalty/mcp-server 1.1.0 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) 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.js +10 -0
  5. package/dist/config.d.ts +0 -13
  6. package/dist/config.js +0 -14
  7. package/dist/http.js +35 -3
  8. package/dist/instructions.d.ts +5 -0
  9. package/dist/instructions.js +440 -0
  10. package/dist/prompts/fan-engagement-setup.d.ts +107 -0
  11. package/dist/prompts/fan-engagement-setup.js +492 -0
  12. package/dist/server.d.ts +1 -1
  13. package/dist/server.js +60 -273
  14. package/dist/tools/achievement/handlers.d.ts +117 -0
  15. package/dist/tools/achievement/handlers.js +161 -0
  16. package/dist/tools/achievement/index.d.ts +479 -0
  17. package/dist/tools/achievement/index.js +74 -0
  18. package/dist/tools/achievement/schemas.d.ts +433 -0
  19. package/dist/tools/achievement/schemas.js +142 -0
  20. package/dist/tools/achievement.d.ts +141 -121
  21. package/dist/tools/achievement.js +60 -24
  22. package/dist/tools/admin.d.ts +6 -6
  23. package/dist/tools/admin.js +12 -12
  24. package/dist/tools/analytics.d.ts +11 -11
  25. package/dist/tools/analytics.js +30 -29
  26. package/dist/tools/apikey.d.ts +3 -3
  27. package/dist/tools/apikey.js +6 -6
  28. package/dist/tools/audit.d.ts +2 -2
  29. package/dist/tools/audit.js +4 -4
  30. package/dist/tools/badge.d.ts +6 -6
  31. package/dist/tools/badge.js +23 -18
  32. package/dist/tools/campaign/handlers.d.ts +42 -0
  33. package/dist/tools/campaign/handlers.js +223 -0
  34. package/dist/tools/campaign/index.d.ts +783 -0
  35. package/dist/tools/campaign/index.js +117 -0
  36. package/dist/tools/campaign/member-handlers.d.ts +60 -0
  37. package/dist/tools/campaign/member-handlers.js +159 -0
  38. package/dist/tools/campaign/schemas.d.ts +704 -0
  39. package/dist/tools/campaign/schemas.js +259 -0
  40. package/dist/tools/campaign/types.d.ts +161 -0
  41. package/dist/tools/campaign/types.js +2 -0
  42. package/dist/tools/custom-event.d.ts +315 -0
  43. package/dist/tools/custom-event.js +270 -0
  44. package/dist/tools/export.d.ts +4 -4
  45. package/dist/tools/export.js +12 -12
  46. package/dist/tools/import.d.ts +3 -3
  47. package/dist/tools/import.js +23 -15
  48. package/dist/tools/index.js +13 -5
  49. package/dist/tools/member/handlers.d.ts +111 -0
  50. package/dist/tools/member/handlers.js +206 -0
  51. package/dist/tools/member/index.d.ts +169 -0
  52. package/dist/tools/member/index.js +92 -0
  53. package/dist/tools/member/schemas.d.ts +89 -0
  54. package/dist/tools/member/schemas.js +65 -0
  55. package/dist/tools/points.d.ts +7 -6
  56. package/dist/tools/points.js +21 -20
  57. package/dist/tools/referral/handlers.d.ts +47 -0
  58. package/dist/tools/referral/handlers.js +115 -0
  59. package/dist/tools/referral/index.d.ts +44 -0
  60. package/dist/tools/referral/index.js +44 -0
  61. package/dist/tools/referral/schemas.d.ts +34 -0
  62. package/dist/tools/referral/schemas.js +52 -0
  63. package/dist/tools/reward/handlers.d.ts +110 -0
  64. package/dist/tools/reward/handlers.js +289 -0
  65. package/dist/tools/reward/index.d.ts +177 -0
  66. package/dist/tools/reward/index.js +93 -0
  67. package/dist/tools/reward/schemas.d.ts +116 -0
  68. package/dist/tools/reward/schemas.js +92 -0
  69. package/dist/tools/role.d.ts +6 -6
  70. package/dist/tools/role.js +12 -12
  71. package/dist/tools/segment/handlers.d.ts +87 -0
  72. package/dist/tools/segment/handlers.js +174 -0
  73. package/dist/tools/segment/index.d.ts +395 -0
  74. package/dist/tools/segment/index.js +88 -0
  75. package/dist/tools/segment/schemas.d.ts +337 -0
  76. package/dist/tools/segment/schemas.js +79 -0
  77. package/dist/tools/segment.d.ts +10 -10
  78. package/dist/tools/segment.js +55 -31
  79. package/dist/tools/store.d.ts +4 -4
  80. package/dist/tools/store.js +8 -8
  81. package/dist/tools/tierset.d.ts +10 -10
  82. package/dist/tools/tierset.js +69 -37
  83. package/dist/tools/transaction.d.ts +4 -4
  84. package/dist/tools/transaction.js +12 -12
  85. package/dist/tools/wallet-type.d.ts +221 -16
  86. package/dist/tools/wallet-type.js +248 -17
  87. package/dist/tools/webhook.d.ts +6 -6
  88. package/dist/tools/webhook.js +90 -31
  89. package/dist/types/schemas/achievement.d.ts +18 -18
  90. package/dist/types/schemas/campaign.d.ts +64 -184
  91. package/dist/types/schemas/campaign.js +2 -7
  92. package/dist/types/schemas/common.d.ts +5 -0
  93. package/dist/types/schemas/common.js +5 -0
  94. package/dist/types/schemas/member.d.ts +2 -2
  95. package/dist/types/schemas/reward.d.ts +94 -18
  96. package/dist/types/schemas/reward.js +8 -3
  97. package/dist/types/schemas/wallet-type.d.ts +306 -8
  98. package/dist/types/schemas/wallet-type.js +82 -1
  99. package/dist/utils/errors.js +32 -5
  100. package/dist/workflows/app-login-streak.d.ts +39 -0
  101. package/dist/workflows/app-login-streak.js +298 -0
  102. package/dist/workflows/early-arrival.d.ts +33 -0
  103. package/dist/workflows/early-arrival.js +148 -0
  104. package/dist/workflows/index.d.ts +101 -0
  105. package/dist/workflows/index.js +208 -0
  106. package/dist/workflows/match-attendance.d.ts +45 -0
  107. package/dist/workflows/match-attendance.js +308 -0
  108. package/dist/workflows/sportsbar-visit.d.ts +41 -0
  109. package/dist/workflows/sportsbar-visit.js +284 -0
  110. package/dist/workflows/vod-watching.d.ts +43 -0
  111. package/dist/workflows/vod-watching.js +326 -0
  112. package/package.json +8 -2
@@ -1,15 +1,36 @@
1
- import { z } from "zod";
2
- import { apiGet } from "../client/http.js";
3
- import { WalletTypeSchema, WalletTypeListResponseSchema, } from "../types/schemas/wallet-type.js";
4
- import { formatApiError } from "../utils/errors.js";
1
+ import axios from "axios";
2
+ import { apiGet, apiPost, apiPut } from "../client/http.js";
3
+ import { WalletTypeSchema, WalletTypeListResponseSchema, WalletTypeCreateResponseSchema, WalletTypeListInputSchema, WalletTypeGetInputSchema, WalletTypeCreateInputSchema, WalletTypeUpdateInputSchema, } from "../types/schemas/wallet-type.js";
4
+ import { formatApiError, OpenLoyaltyError } from "../utils/errors.js";
5
5
  import { getStoreCode } from "../config.js";
6
- export const WalletTypeListInputSchema = {
7
- storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
8
- };
9
- export const WalletTypeGetInputSchema = {
10
- storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
11
- walletTypeId: z.string().describe("The wallet type ID to retrieve."),
12
- };
6
+ import { omitUndefined } from "../utils/payload.js";
7
+ // Re-export input schemas for tool registration
8
+ export { WalletTypeListInputSchema, WalletTypeGetInputSchema, WalletTypeCreateInputSchema, WalletTypeUpdateInputSchema, };
9
+ // Helper to check if error is an axios error (more reliable than instanceof)
10
+ function isAxiosError(error) {
11
+ return axios.isAxiosError(error);
12
+ }
13
+ // Helper to extract error message from axios or regular errors
14
+ function getErrorMessage(error) {
15
+ if (isAxiosError(error)) {
16
+ const data = error.response?.data;
17
+ return data?.message ? String(data.message) : error.message;
18
+ }
19
+ if (error instanceof Error) {
20
+ return error.message;
21
+ }
22
+ return String(error);
23
+ }
24
+ // Helper to get HTTP status from error
25
+ function getErrorStatus(error) {
26
+ if (isAxiosError(error)) {
27
+ return error.response?.status;
28
+ }
29
+ return undefined;
30
+ }
31
+ // =============================================================================
32
+ // HANDLER FUNCTIONS
33
+ // =============================================================================
13
34
  export async function walletTypeList(input) {
14
35
  const storeCode = getStoreCode(input.storeCode);
15
36
  try {
@@ -26,7 +47,7 @@ export async function walletTypeList(input) {
26
47
  }));
27
48
  }
28
49
  catch (error) {
29
- throw formatApiError(error, "openloyalty_wallet_type_list");
50
+ throw formatApiError(error, "ol_wallet_type_list");
30
51
  }
31
52
  }
32
53
  export async function walletTypeGet(input) {
@@ -37,24 +58,234 @@ export async function walletTypeGet(input) {
37
58
  return validated;
38
59
  }
39
60
  catch (error) {
40
- throw formatApiError(error, "openloyalty_wallet_type_get");
61
+ // Check for 404 errors
62
+ if (getErrorStatus(error) === 404) {
63
+ throw new OpenLoyaltyError({
64
+ code: "NOT_FOUND",
65
+ message: `Wallet type with ID '${input.walletTypeId}' not found`,
66
+ hint: `Use ol_wallet_type_list() to see available wallet types. ` +
67
+ `The walletTypeId may be incorrect or the wallet type may have been deleted.`,
68
+ relatedTool: "ol_wallet_type_get",
69
+ });
70
+ }
71
+ throw formatApiError(error, "ol_wallet_type_get");
72
+ }
73
+ }
74
+ export async function walletTypeCreate(input) {
75
+ const storeCode = getStoreCode(input.storeCode);
76
+ // Validate required fields
77
+ if (!input.translations?.en?.name) {
78
+ throw new OpenLoyaltyError({
79
+ code: "VALIDATION_ERROR",
80
+ message: "Missing required field: translations.en.name",
81
+ hint: "translations.en.name is REQUIRED. Example: { translations: { en: { name: 'Bonus Points' } } }",
82
+ relatedTool: "ol_wallet_type_create",
83
+ });
84
+ }
85
+ if (!input.unitSingularName || !input.unitPluralName) {
86
+ throw new OpenLoyaltyError({
87
+ code: "VALIDATION_ERROR",
88
+ message: "Missing required fields: unitSingularName and/or unitPluralName",
89
+ hint: "Both unitSingularName (e.g., 'point') and unitPluralName (e.g., 'points') are REQUIRED.",
90
+ relatedTool: "ol_wallet_type_create",
91
+ });
92
+ }
93
+ // Validate unitExpiryDate format if provided
94
+ if (input.unitExpiryDate && !/^\d{2}-\d{2}$/.test(input.unitExpiryDate)) {
95
+ throw new OpenLoyaltyError({
96
+ code: "VALIDATION_ERROR",
97
+ message: `Invalid unitExpiryDate format: '${input.unitExpiryDate}'`,
98
+ hint: "unitExpiryDate must be in 'MM-DD' format (e.g., '12-31' for December 31st). " +
99
+ "Do NOT use 'YYYY-MM-DD' format.",
100
+ relatedTool: "ol_wallet_type_create",
101
+ });
102
+ }
103
+ // Validate unitDaysExpiryAfter is provided (API requires it)
104
+ if (!input.unitDaysExpiryAfter) {
105
+ throw new OpenLoyaltyError({
106
+ code: "VALIDATION_ERROR",
107
+ message: "Missing required field: unitDaysExpiryAfter",
108
+ hint: "unitDaysExpiryAfter is REQUIRED. Use 'all_time_active' for never-expiring points, " +
109
+ "or a number string like '365' for points that expire after N days.",
110
+ relatedTool: "ol_wallet_type_create",
111
+ });
112
+ }
113
+ // Build payload - API expects { walletType: { ... } }
114
+ // NOTE: 'active' and 'limits' are NOT accepted at creation time - use update after creation
115
+ const walletTypePayload = omitUndefined({
116
+ translations: input.translations,
117
+ unitSingularName: input.unitSingularName,
118
+ unitPluralName: input.unitPluralName,
119
+ code: input.code,
120
+ allowNegativeBalance: input.allowNegativeBalance,
121
+ unitExpiryDate: input.unitExpiryDate,
122
+ unitDaysExpiryAfter: input.unitDaysExpiryAfter,
123
+ unitDaysActiveCount: input.unitDaysActiveCount,
124
+ unitYearsActiveCount: input.unitYearsActiveCount,
125
+ unitDaysLocked: input.unitDaysLocked,
126
+ allTimeNotLocked: input.allTimeNotLocked,
127
+ });
128
+ try {
129
+ const response = await apiPost(`/${storeCode}/walletType`, { walletType: walletTypePayload });
130
+ const validated = WalletTypeCreateResponseSchema.parse(response);
131
+ return {
132
+ walletTypeId: validated.walletTypeId,
133
+ name: input.translations.en.name,
134
+ code: input.code,
135
+ };
136
+ }
137
+ catch (error) {
138
+ // Check for duplicate code error
139
+ const errorMsg = getErrorMessage(error);
140
+ if (errorMsg.toLowerCase().includes("code") && errorMsg.toLowerCase().includes("already")) {
141
+ throw new OpenLoyaltyError({
142
+ code: "DUPLICATE_CODE",
143
+ message: `Wallet type with code '${input.code}' already exists`,
144
+ hint: `Use a different code value, or use ol_wallet_type_list() to find existing wallet types. ` +
145
+ `Each wallet type must have a unique code.`,
146
+ relatedTool: "ol_wallet_type_create",
147
+ });
148
+ }
149
+ throw formatApiError(error, "ol_wallet_type_create");
41
150
  }
42
151
  }
152
+ export async function walletTypeUpdate(input) {
153
+ const storeCode = getStoreCode(input.storeCode);
154
+ // Validate unitExpiryDate format if provided
155
+ if (input.unitExpiryDate && !/^\d{2}-\d{2}$/.test(input.unitExpiryDate)) {
156
+ throw new OpenLoyaltyError({
157
+ code: "VALIDATION_ERROR",
158
+ message: `Invalid unitExpiryDate format: '${input.unitExpiryDate}'`,
159
+ hint: "unitExpiryDate must be in 'MM-DD' format (e.g., '12-31' for December 31st). " +
160
+ "Do NOT use 'YYYY-MM-DD' format.",
161
+ relatedTool: "ol_wallet_type_update",
162
+ });
163
+ }
164
+ // Validate interval types if limits provided
165
+ const validIntervalTypes = ["calendarHours", "calendarDays", "calendarWeeks", "calendarMonths", "calendarYears"];
166
+ if (input.limits?.points?.interval?.type && !validIntervalTypes.includes(input.limits.points.interval.type)) {
167
+ throw new OpenLoyaltyError({
168
+ code: "VALIDATION_ERROR",
169
+ message: `Invalid interval type: '${input.limits.points.interval.type}'`,
170
+ hint: `Valid interval types are: ${validIntervalTypes.join(", ")}. ` +
171
+ `IMPORTANT: Use 'calendarDays' NOT 'days', 'calendarMonths' NOT 'months', etc.`,
172
+ relatedTool: "ol_wallet_type_update",
173
+ });
174
+ }
175
+ // API requires full wallet type object - fetch existing then merge
176
+ let existing;
177
+ try {
178
+ existing = await apiGet(`/${storeCode}/walletType/${input.walletTypeId}`);
179
+ }
180
+ catch (error) {
181
+ // Check for 404 errors
182
+ if (getErrorStatus(error) === 404) {
183
+ throw new OpenLoyaltyError({
184
+ code: "NOT_FOUND",
185
+ message: `Wallet type with ID '${input.walletTypeId}' not found`,
186
+ hint: `Use ol_wallet_type_list() to see available wallet types. ` +
187
+ `The walletTypeId may be incorrect or the wallet type may have been deleted.`,
188
+ relatedTool: "ol_wallet_type_update",
189
+ });
190
+ }
191
+ throw formatApiError(error, "ol_wallet_type_update");
192
+ }
193
+ // Build translations - merge with existing
194
+ const existingTranslations = existing.translations || {};
195
+ const existingEn = existingTranslations.en || {};
196
+ const updatedTranslations = {
197
+ ...existingTranslations,
198
+ en: {
199
+ ...existingEn,
200
+ ...(input.name !== undefined ? { name: input.name } : {}),
201
+ ...(input.description !== undefined ? { description: input.description } : {}),
202
+ },
203
+ };
204
+ // Build payload with existing values as base
205
+ const walletTypePayload = {
206
+ translations: updatedTranslations,
207
+ unitSingularName: input.unitSingularName ?? existing.unitSingularName,
208
+ unitPluralName: input.unitPluralName ?? existing.unitPluralName,
209
+ active: input.active ?? existing.active,
210
+ allowNegativeBalance: input.allowNegativeBalance ?? existing.allowNegativeBalance,
211
+ limits: input.limits ?? existing.limits,
212
+ unitExpiryDate: input.unitExpiryDate ?? existing.unitExpiryDate,
213
+ unitDaysExpiryAfter: input.unitDaysExpiryAfter ?? existing.unitDaysExpiryAfter,
214
+ unitDaysActiveCount: input.unitDaysActiveCount ?? existing.unitDaysActiveCount,
215
+ unitYearsActiveCount: input.unitYearsActiveCount ?? existing.unitYearsActiveCount,
216
+ unitDaysLocked: input.unitDaysLocked ?? existing.unitDaysLocked,
217
+ allTimeNotLocked: input.allTimeNotLocked ?? existing.allTimeNotLocked,
218
+ };
219
+ try {
220
+ await apiPut(`/${storeCode}/walletType/${input.walletTypeId}`, { walletType: walletTypePayload });
221
+ }
222
+ catch (error) {
223
+ throw formatApiError(error, "ol_wallet_type_update");
224
+ }
225
+ }
226
+ // =============================================================================
227
+ // TOOL DEFINITIONS
228
+ // =============================================================================
43
229
  export const walletTypeToolDefinitions = [
44
230
  {
45
- name: "openloyalty_wallet_type_list",
231
+ name: "ol_wallet_type_list",
46
232
  title: "List Point Currencies",
47
- description: "List all available wallet types (point currencies). Use this to find walletCode values for tier conditions. Returns walletTypeId, code, and name for each wallet type.",
233
+ description: "List all available wallet types (point currencies). " +
234
+ "Use this to find wallet type codes and IDs for other operations. " +
235
+ "Returns walletTypeId (UUID), code (unique identifier like 'default'), and name for each wallet type. " +
236
+ "💡 TIP: Most stores have a 'default' wallet type for main loyalty points.",
48
237
  readOnly: true,
49
238
  inputSchema: WalletTypeListInputSchema,
50
239
  handler: walletTypeList,
51
240
  },
52
241
  {
53
- name: "openloyalty_wallet_type_get",
242
+ name: "ol_wallet_type_get",
54
243
  title: "Get Point Currency Details",
55
- description: "Get details for a specific wallet type by ID. Returns the full wallet type configuration including currency names.",
244
+ description: "Get full details for a specific wallet type by ID. " +
245
+ "Returns configuration including limits, expiry settings, and translations. " +
246
+ "💡 TIP: Use ol_wallet_type_list() first to find the walletTypeId.",
56
247
  readOnly: true,
57
248
  inputSchema: WalletTypeGetInputSchema,
58
249
  handler: walletTypeGet,
59
250
  },
251
+ {
252
+ name: "ol_wallet_type_create",
253
+ title: "Create Point Currency",
254
+ description: "Create a new wallet type (point currency) for the loyalty program. " +
255
+ "⚠️ REQUIRED FIELDS (will fail without these): " +
256
+ "1. translations: { en: { name: 'Currency Name' } } - Name is REQUIRED. " +
257
+ "2. unitSingularName: 'point' (or 'coin', 'star', etc.) - The singular form. " +
258
+ "3. unitPluralName: 'points' (or 'coins', 'stars', etc.) - The plural form. " +
259
+ "4. unitDaysExpiryAfter: 'all_time_active' or number string like '365'. " +
260
+ "📝 OPTIONAL: " +
261
+ "• code: Unique identifier (auto-generated if omitted, cannot change later). " +
262
+ "• allowNegativeBalance: true/false (default: false). " +
263
+ "• unitExpiryDate: Annual expiry in 'MM-DD' format (e.g., '12-31'). " +
264
+ "• unitDaysLocked: Days before points become spendable (0 for immediate). " +
265
+ "⚠️ NOT SUPPORTED AT CREATION: 'active' and 'limits' - use ol_wallet_type_update after creation. " +
266
+ "⏰ NOTE: New wallets are 'blocked' for ~2 minutes after creation - wait before updating. " +
267
+ "💡 EXAMPLE: { translations: { en: { name: 'Bonus Points' } }, unitSingularName: 'point', " +
268
+ "unitPluralName: 'points', unitDaysExpiryAfter: 'all_time_active', code: 'bonus' }",
269
+ readOnly: false,
270
+ inputSchema: WalletTypeCreateInputSchema,
271
+ handler: walletTypeCreate,
272
+ },
273
+ {
274
+ name: "ol_wallet_type_update",
275
+ title: "Update Point Currency",
276
+ description: "Update an existing wallet type's configuration. " +
277
+ "⚠️ IMPORTANT: Only provide fields you want to change - others are preserved. " +
278
+ "⚠️ CANNOT CHANGE: The 'code' field cannot be modified after creation. " +
279
+ "📝 UPDATABLE FIELDS: " +
280
+ "• name/description: Update via the tool parameters (updates translations.en internally). " +
281
+ "• unitSingularName/unitPluralName: Update the display names. " +
282
+ "• active: Enable/disable the wallet type. " +
283
+ "• allowNegativeBalance: Allow/disallow negative balances. " +
284
+ "• limits: Update earning limits (same format as create). " +
285
+ "• Expiry settings: unitExpiryDate, unitDaysExpiryAfter, etc. " +
286
+ "💡 TIP: Use ol_wallet_type_get(walletTypeId) first to see current configuration.",
287
+ readOnly: false,
288
+ inputSchema: WalletTypeUpdateInputSchema,
289
+ handler: walletTypeUpdate,
290
+ },
60
291
  ];
@@ -104,7 +104,7 @@ export declare function webhookEvents(input: {
104
104
  storeCode?: string;
105
105
  }): Promise<WebhookEventTypesResponse>;
106
106
  export declare const webhookToolDefinitions: readonly [{
107
- readonly name: "openloyalty_webhook_list";
107
+ readonly name: "ol_webhook_list";
108
108
  readonly title: "List Webhook Subscriptions";
109
109
  readonly description: "List webhook subscriptions with optional filtering. Returns paginated list of subscriptions with webhookSubscriptionId, eventName, url, and createdAt.";
110
110
  readonly readOnly: true;
@@ -117,7 +117,7 @@ export declare const webhookToolDefinitions: readonly [{
117
117
  };
118
118
  readonly handler: typeof webhookList;
119
119
  }, {
120
- readonly name: "openloyalty_webhook_create";
120
+ readonly name: "ol_webhook_create";
121
121
  readonly title: "Create Webhook Subscription";
122
122
  readonly description: "Create a new webhook subscription to receive event notifications. Returns webhookSubscriptionId on success. Use webhook_events to discover available event types before creating subscriptions.";
123
123
  readonly readOnly: false;
@@ -138,7 +138,7 @@ export declare const webhookToolDefinitions: readonly [{
138
138
  };
139
139
  readonly handler: typeof webhookCreate;
140
140
  }, {
141
- readonly name: "openloyalty_webhook_get";
141
+ readonly name: "ol_webhook_get";
142
142
  readonly title: "Get Webhook Subscription Details";
143
143
  readonly description: "Get full webhook subscription details including headers configuration.";
144
144
  readonly readOnly: true;
@@ -148,7 +148,7 @@ export declare const webhookToolDefinitions: readonly [{
148
148
  };
149
149
  readonly handler: typeof webhookGet;
150
150
  }, {
151
- readonly name: "openloyalty_webhook_update";
151
+ readonly name: "ol_webhook_update";
152
152
  readonly title: "Update Webhook Subscription";
153
153
  readonly description: "Update a webhook subscription. Can update eventName, url, and headers. Returns void on success (204 No Content).";
154
154
  readonly readOnly: false;
@@ -170,7 +170,7 @@ export declare const webhookToolDefinitions: readonly [{
170
170
  };
171
171
  readonly handler: typeof webhookUpdate;
172
172
  }, {
173
- readonly name: "openloyalty_webhook_delete";
173
+ readonly name: "ol_webhook_delete";
174
174
  readonly title: "Delete Webhook Subscription (Permanent)";
175
175
  readonly description: "Delete a webhook subscription. Returns void on success (204 No Content). The subscription will stop receiving events immediately.";
176
176
  readonly readOnly: false;
@@ -181,7 +181,7 @@ export declare const webhookToolDefinitions: readonly [{
181
181
  };
182
182
  readonly handler: typeof webhookDelete;
183
183
  }, {
184
- readonly name: "openloyalty_webhook_events";
184
+ readonly name: "ol_webhook_events";
185
185
  readonly title: "List Available Webhook Events";
186
186
  readonly description: "Get available webhook event types. Returns list of event names that can be used when creating webhook subscriptions. Use this to discover available events before creating subscriptions.";
187
187
  readonly readOnly: true;
@@ -1,7 +1,68 @@
1
1
  import { z } from "zod";
2
+ import { isIP } from "net";
2
3
  import { apiGet, apiPost, apiPut, apiDelete } from "../client/http.js";
3
4
  import { formatApiError } from "../utils/errors.js";
4
5
  import { getStoreCode } from "../config.js";
6
+ /**
7
+ * Check if an IP address is private, loopback, link-local, or otherwise internal.
8
+ * Blocks IPv4 and IPv6 private ranges to prevent SSRF attacks.
9
+ */
10
+ function isPrivateIP(ip) {
11
+ const normalizedIP = ip.toLowerCase();
12
+ // IPv4 private/reserved ranges
13
+ if (normalizedIP.startsWith("10.") || // 10.0.0.0/8 private
14
+ normalizedIP.startsWith("192.168.") || // 192.168.0.0/16 private
15
+ normalizedIP.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./) || // 172.16.0.0/12 private
16
+ normalizedIP.startsWith("127.") || // 127.0.0.0/8 loopback
17
+ normalizedIP.startsWith("169.254.") || // 169.254.0.0/16 link-local
18
+ normalizedIP === "0.0.0.0" || // Unspecified
19
+ normalizedIP.startsWith("100.64.") || // 100.64.0.0/10 carrier-grade NAT
20
+ normalizedIP.match(/^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\./) // 100.64-127.x.x
21
+ ) {
22
+ return true;
23
+ }
24
+ // IPv6 private/reserved ranges
25
+ if (normalizedIP === "::1" || // Loopback
26
+ normalizedIP === "::" || // Unspecified
27
+ normalizedIP.startsWith("fc") || // fc00::/7 unique local (fc00-fdff)
28
+ normalizedIP.startsWith("fd") || // fc00::/7 unique local (fc00-fdff)
29
+ normalizedIP.startsWith("fe80:") || // fe80::/10 link-local
30
+ normalizedIP.startsWith("::ffff:127.") || // IPv4-mapped loopback
31
+ normalizedIP.startsWith("::ffff:10.") || // IPv4-mapped private
32
+ normalizedIP.startsWith("::ffff:192.168.") || // IPv4-mapped private
33
+ normalizedIP.match(/^::ffff:172\.(1[6-9]|2[0-9]|3[0-1])\./) // IPv4-mapped private
34
+ ) {
35
+ return true;
36
+ }
37
+ return false;
38
+ }
39
+ /**
40
+ * Check if a hostname points to an internal DNS name.
41
+ * Blocks common internal TLDs and patterns.
42
+ */
43
+ function isInternalHostname(hostname) {
44
+ const lowerHostname = hostname.toLowerCase();
45
+ // Block localhost variants
46
+ if (lowerHostname === "localhost" ||
47
+ lowerHostname.endsWith(".localhost") ||
48
+ lowerHostname.endsWith(".local") ||
49
+ lowerHostname.endsWith(".internal") ||
50
+ lowerHostname.endsWith(".lan") ||
51
+ lowerHostname.endsWith(".home") ||
52
+ lowerHostname.endsWith(".corp") ||
53
+ lowerHostname.endsWith(".intranet")) {
54
+ return true;
55
+ }
56
+ // Block cloud metadata hostnames
57
+ if (lowerHostname === "metadata.google.internal" ||
58
+ lowerHostname === "metadata" ||
59
+ lowerHostname.endsWith(".metadata") ||
60
+ lowerHostname === "instance-data" ||
61
+ lowerHostname.endsWith(".amazonaws.com") && lowerHostname.includes("metadata")) {
62
+ return true;
63
+ }
64
+ return false;
65
+ }
5
66
  // SSRF protection: validate webhook URLs are external HTTPS endpoints
6
67
  function isValidWebhookUrl(url) {
7
68
  try {
@@ -11,24 +72,22 @@ function isValidWebhookUrl(url) {
11
72
  return false;
12
73
  }
13
74
  const hostname = parsed.hostname.toLowerCase();
14
- // Block localhost and loopback
15
- if (hostname === "localhost" ||
16
- hostname === "127.0.0.1" ||
17
- hostname === "0.0.0.0" ||
18
- hostname === "::1" ||
19
- hostname.endsWith(".localhost")) {
75
+ // Block internal hostnames
76
+ if (isInternalHostname(hostname)) {
20
77
  return false;
21
78
  }
22
- // Block cloud metadata endpoints
23
- if (hostname === "169.254.169.254" || hostname === "metadata.google.internal") {
24
- return false;
79
+ // If hostname is an IP address, validate it directly
80
+ if (isIP(hostname)) {
81
+ return !isPrivateIP(hostname);
25
82
  }
26
- // Block private IP ranges (basic check for common patterns)
27
- if (hostname.startsWith("10.") ||
28
- hostname.startsWith("192.168.") ||
29
- hostname.match(/^172\.(1[6-9]|2[0-9]|3[0-1])\./)) {
83
+ // Block cloud metadata IP
84
+ if (hostname === "169.254.169.254") {
30
85
  return false;
31
86
  }
87
+ // For DNS hostnames, we can't resolve at validation time (sync function),
88
+ // but we've blocked common internal patterns above.
89
+ // Note: Full DNS rebinding protection would require async DNS resolution,
90
+ // which could be added if this becomes a concern in production.
32
91
  return true;
33
92
  }
34
93
  catch {
@@ -40,14 +99,14 @@ const webhookUrlSchema = z.string()
40
99
  .refine(isValidWebhookUrl, "URL must be an external HTTPS endpoint (no localhost, private IPs, or metadata endpoints)");
41
100
  // Input Schemas
42
101
  export const WebhookListInputSchema = {
43
- storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
102
+ storeCode: z.string().optional().describe("Store code for multi-tenant routing. DO NOT pass this parameter - the configured default will be used automatically. Only provide a value if the user explicitly asks to work with a different store."),
44
103
  page: z.number().optional().describe("Page number (default: 1)."),
45
104
  perPage: z.number().optional().describe("Items per page (default: 25)."),
46
105
  eventName: z.string().optional().describe("Filter by event name."),
47
106
  url: z.string().optional().describe("Filter by URL."),
48
107
  };
49
108
  export const WebhookCreateInputSchema = {
50
- storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
109
+ storeCode: z.string().optional().describe("Store code for multi-tenant routing. DO NOT pass this parameter - the configured default will be used automatically. Only provide a value if the user explicitly asks to work with a different store."),
51
110
  eventName: z.string().describe("Event name to subscribe to. Use webhook_events to discover available events."),
52
111
  url: webhookUrlSchema.describe("HTTPS URL to receive webhook events. Must be an external endpoint (no localhost or private IPs)."),
53
112
  headers: z.array(z.object({
@@ -56,11 +115,11 @@ export const WebhookCreateInputSchema = {
56
115
  })).optional().describe("Custom headers to include in webhook requests."),
57
116
  };
58
117
  export const WebhookGetInputSchema = {
59
- storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
118
+ storeCode: z.string().optional().describe("Store code for multi-tenant routing. DO NOT pass this parameter - the configured default will be used automatically. Only provide a value if the user explicitly asks to work with a different store."),
60
119
  webhookSubscriptionId: z.string().describe("The webhook subscription ID (UUID) to retrieve."),
61
120
  };
62
121
  export const WebhookUpdateInputSchema = {
63
- storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
122
+ storeCode: z.string().optional().describe("Store code for multi-tenant routing. DO NOT pass this parameter - the configured default will be used automatically. Only provide a value if the user explicitly asks to work with a different store."),
64
123
  webhookSubscriptionId: z.string().describe("The webhook subscription ID (UUID) to update."),
65
124
  eventName: z.string().optional().describe("Event name to subscribe to."),
66
125
  url: webhookUrlSchema.optional().describe("HTTPS URL to receive webhook events. Must be an external endpoint (no localhost or private IPs)."),
@@ -70,11 +129,11 @@ export const WebhookUpdateInputSchema = {
70
129
  })).optional().describe("Custom headers to include in webhook requests."),
71
130
  };
72
131
  export const WebhookDeleteInputSchema = {
73
- storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
132
+ storeCode: z.string().optional().describe("Store code for multi-tenant routing. DO NOT pass this parameter - the configured default will be used automatically. Only provide a value if the user explicitly asks to work with a different store."),
74
133
  webhookSubscriptionId: z.string().describe("The webhook subscription ID (UUID) to delete."),
75
134
  };
76
135
  export const WebhookEventsInputSchema = {
77
- storeCode: z.string().optional().describe("Store code. If not provided, uses the default store code from configuration."),
136
+ storeCode: z.string().optional().describe("Store code for multi-tenant routing. DO NOT pass this parameter - the configured default will be used automatically. Only provide a value if the user explicitly asks to work with a different store."),
78
137
  };
79
138
  // Handler functions
80
139
  export async function webhookList(input) {
@@ -95,7 +154,7 @@ export async function webhookList(input) {
95
154
  return response;
96
155
  }
97
156
  catch (error) {
98
- throw formatApiError(error, "openloyalty_webhook_list");
157
+ throw formatApiError(error, "ol_webhook_list");
99
158
  }
100
159
  }
101
160
  export async function webhookCreate(input) {
@@ -112,7 +171,7 @@ export async function webhookCreate(input) {
112
171
  return response;
113
172
  }
114
173
  catch (error) {
115
- throw formatApiError(error, "openloyalty_webhook_create");
174
+ throw formatApiError(error, "ol_webhook_create");
116
175
  }
117
176
  }
118
177
  export async function webhookGet(input) {
@@ -122,7 +181,7 @@ export async function webhookGet(input) {
122
181
  return response;
123
182
  }
124
183
  catch (error) {
125
- throw formatApiError(error, "openloyalty_webhook_get");
184
+ throw formatApiError(error, "ol_webhook_get");
126
185
  }
127
186
  }
128
187
  export async function webhookUpdate(input) {
@@ -138,7 +197,7 @@ export async function webhookUpdate(input) {
138
197
  await apiPut(`/${storeCode}/webhook/subscription/${input.webhookSubscriptionId}`, { webhookSubscription: payload });
139
198
  }
140
199
  catch (error) {
141
- throw formatApiError(error, "openloyalty_webhook_update");
200
+ throw formatApiError(error, "ol_webhook_update");
142
201
  }
143
202
  }
144
203
  export async function webhookDelete(input) {
@@ -147,7 +206,7 @@ export async function webhookDelete(input) {
147
206
  await apiDelete(`/${storeCode}/webhook/subscription/${input.webhookSubscriptionId}`);
148
207
  }
149
208
  catch (error) {
150
- throw formatApiError(error, "openloyalty_webhook_delete");
209
+ throw formatApiError(error, "ol_webhook_delete");
151
210
  }
152
211
  }
153
212
  export async function webhookEvents(input) {
@@ -157,13 +216,13 @@ export async function webhookEvents(input) {
157
216
  return response;
158
217
  }
159
218
  catch (error) {
160
- throw formatApiError(error, "openloyalty_webhook_events");
219
+ throw formatApiError(error, "ol_webhook_events");
161
220
  }
162
221
  }
163
222
  // Tool definitions
164
223
  export const webhookToolDefinitions = [
165
224
  {
166
- name: "openloyalty_webhook_list",
225
+ name: "ol_webhook_list",
167
226
  title: "List Webhook Subscriptions",
168
227
  description: "List webhook subscriptions with optional filtering. Returns paginated list of subscriptions with webhookSubscriptionId, eventName, url, and createdAt.",
169
228
  readOnly: true,
@@ -171,7 +230,7 @@ export const webhookToolDefinitions = [
171
230
  handler: webhookList,
172
231
  },
173
232
  {
174
- name: "openloyalty_webhook_create",
233
+ name: "ol_webhook_create",
175
234
  title: "Create Webhook Subscription",
176
235
  description: "Create a new webhook subscription to receive event notifications. Returns webhookSubscriptionId on success. Use webhook_events to discover available event types before creating subscriptions.",
177
236
  readOnly: false,
@@ -179,7 +238,7 @@ export const webhookToolDefinitions = [
179
238
  handler: webhookCreate,
180
239
  },
181
240
  {
182
- name: "openloyalty_webhook_get",
241
+ name: "ol_webhook_get",
183
242
  title: "Get Webhook Subscription Details",
184
243
  description: "Get full webhook subscription details including headers configuration.",
185
244
  readOnly: true,
@@ -187,7 +246,7 @@ export const webhookToolDefinitions = [
187
246
  handler: webhookGet,
188
247
  },
189
248
  {
190
- name: "openloyalty_webhook_update",
249
+ name: "ol_webhook_update",
191
250
  title: "Update Webhook Subscription",
192
251
  description: "Update a webhook subscription. Can update eventName, url, and headers. Returns void on success (204 No Content).",
193
252
  readOnly: false,
@@ -195,7 +254,7 @@ export const webhookToolDefinitions = [
195
254
  handler: webhookUpdate,
196
255
  },
197
256
  {
198
- name: "openloyalty_webhook_delete",
257
+ name: "ol_webhook_delete",
199
258
  title: "Delete Webhook Subscription (Permanent)",
200
259
  description: "Delete a webhook subscription. Returns void on success (204 No Content). The subscription will stop receiving events immediately.",
201
260
  readOnly: false,
@@ -204,7 +263,7 @@ export const webhookToolDefinitions = [
204
263
  handler: webhookDelete,
205
264
  },
206
265
  {
207
- name: "openloyalty_webhook_events",
266
+ name: "ol_webhook_events",
208
267
  title: "List Available Webhook Events",
209
268
  description: "Get available webhook event types. Returns list of event names that can be used when creating webhook subscriptions. Use this to discover available events before creating subscriptions.",
210
269
  readOnly: true,