@open-loyalty/mcp-server 1.3.4 → 1.3.6

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.
@@ -100,15 +100,15 @@ export declare const campaignToolDefinitions: readonly [{
100
100
  conditions: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodObject<{
101
101
  operator: import("zod").ZodString;
102
102
  attribute: import("zod").ZodString;
103
- data: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
103
+ data: import("zod").ZodUnknown;
104
104
  }, "strip", import("zod").ZodTypeAny, {
105
105
  attribute: string;
106
106
  operator: string;
107
- data?: Record<string, unknown> | undefined;
107
+ data?: unknown;
108
108
  }, {
109
109
  attribute: string;
110
110
  operator: string;
111
- data?: Record<string, unknown> | undefined;
111
+ data?: unknown;
112
112
  }>, "many">>;
113
113
  }, "strip", import("zod").ZodTypeAny, {
114
114
  name: string;
@@ -124,7 +124,7 @@ export declare const campaignToolDefinitions: readonly [{
124
124
  conditions?: {
125
125
  attribute: string;
126
126
  operator: string;
127
- data?: Record<string, unknown> | undefined;
127
+ data?: unknown;
128
128
  }[] | undefined;
129
129
  target?: "self" | "referrer" | undefined;
130
130
  }, {
@@ -141,7 +141,7 @@ export declare const campaignToolDefinitions: readonly [{
141
141
  conditions?: {
142
142
  attribute: string;
143
143
  operator: string;
144
- data?: Record<string, unknown> | undefined;
144
+ data?: unknown;
145
145
  }[] | undefined;
146
146
  target?: "self" | "referrer" | undefined;
147
147
  }>, "many">;
@@ -27,8 +27,10 @@ export const campaignToolDefinitions = [
27
27
  "3. translations: { en: { name: 'Campaign Name' } }, " +
28
28
  "4. activity: { startsAt: '2026-01-01 00:00+00:00' }, " +
29
29
  "5. rules: [{ name: 'Rule Name', effects: [{ effect: 'give_points', pointsRule: 'transaction.grossValue * 10' }] }]. " +
30
- "pointsRule is a STRING expression, not an object. Examples: '100' (fixed), 'transaction.grossValue * 10' (dynamic). " +
31
- "To target ALL members, OMIT the audience parameter entirely - do not set audience.target='all'.",
30
+ "pointsRule is a STRING expression. Examples: '100' (fixed), 'transaction.grossValue * 10' (dynamic). " +
31
+ "CONDITIONS: Use operator='is_greater_or_equal' (NOT 'gte'), attribute='transaction.grossValue', data=100. " +
32
+ "For labels: operator='has_at_least_one_label', attribute='transaction.labels', data=[{key:'name',value:'val'}]. " +
33
+ "To target ALL members, OMIT the audience parameter entirely.",
32
34
  readOnly: false,
33
35
  inputSchema: CampaignCreateInputSchema,
34
36
  handler: campaignCreate,
@@ -495,15 +495,15 @@ export declare const CampaignCreateInputSchema: {
495
495
  conditions: z.ZodOptional<z.ZodArray<z.ZodObject<{
496
496
  operator: z.ZodString;
497
497
  attribute: z.ZodString;
498
- data: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
498
+ data: z.ZodUnknown;
499
499
  }, "strip", z.ZodTypeAny, {
500
500
  attribute: string;
501
501
  operator: string;
502
- data?: Record<string, unknown> | undefined;
502
+ data?: unknown;
503
503
  }, {
504
504
  attribute: string;
505
505
  operator: string;
506
- data?: Record<string, unknown> | undefined;
506
+ data?: unknown;
507
507
  }>, "many">>;
508
508
  }, "strip", z.ZodTypeAny, {
509
509
  name: string;
@@ -519,7 +519,7 @@ export declare const CampaignCreateInputSchema: {
519
519
  conditions?: {
520
520
  attribute: string;
521
521
  operator: string;
522
- data?: Record<string, unknown> | undefined;
522
+ data?: unknown;
523
523
  }[] | undefined;
524
524
  target?: "self" | "referrer" | undefined;
525
525
  }, {
@@ -536,7 +536,7 @@ export declare const CampaignCreateInputSchema: {
536
536
  conditions?: {
537
537
  attribute: string;
538
538
  operator: string;
539
- data?: Record<string, unknown> | undefined;
539
+ data?: unknown;
540
540
  }[] | undefined;
541
541
  target?: "self" | "referrer" | undefined;
542
542
  }>, "many">;
@@ -25,10 +25,23 @@ const CampaignEffectInputSchema = z.object({
25
25
  customAttributeValueRule: z.string().optional().describe("Custom attribute value rule."),
26
26
  });
27
27
  // Condition input schema
28
+ // Valid operators: is_equal, is_not_equal, is_greater, is_greater_or_equal, is_less, is_less_or_equal,
29
+ // contains, not_contains, contains_one_of, not_contains_one_of, has_at_least_one_label,
30
+ // is_one_of, is_not_one_of, starts_with, ends_with, matches_regex,
31
+ // is_day_of_week, is_month_of_year, is_day_of_month, is_between, is_not_between, is_after, is_before
28
32
  const CampaignConditionInputSchema = z.object({
29
- operator: z.string().describe("Condition operator (is_equal, gte, lte, etc.)."),
30
- attribute: z.string().describe("Attribute to check."),
31
- data: z.record(z.unknown()).optional().describe("Condition-specific data."),
33
+ operator: z.string().describe("Condition operator. Valid values: " +
34
+ "is_equal, is_not_equal, is_greater, is_greater_or_equal, is_less, is_less_or_equal, " +
35
+ "contains, not_contains, has_at_least_one_label, is_one_of, is_not_one_of, " +
36
+ "starts_with, ends_with, matches_regex, is_between, is_not_between, is_after, is_before. " +
37
+ "⚠️ Do NOT use 'gte', 'eq' - use full names like 'is_greater_or_equal', 'is_equal'."),
38
+ attribute: z.string().describe("Attribute path to check (e.g., 'transaction.grossValue', 'transaction.labels', 'customer.email'). " +
39
+ "Use full path with context prefix."),
40
+ data: z.unknown().describe("Condition value. Format depends on operator: " +
41
+ "• Number for is_greater/is_less/etc: 100 " +
42
+ "• String for contains/starts_with/etc: 'value' " +
43
+ "• Array for is_one_of/contains_one_of: ['a', 'b'] " +
44
+ "• Label array for has_at_least_one_label: [{key: 'name', value: 'val'}]"),
32
45
  });
33
46
  // Rule input schema
34
47
  const CampaignRuleInputSchema = z.object({
@@ -82,12 +82,15 @@ export async function pointsSpend(input) {
82
82
  catch (error) {
83
83
  // Check for insufficient points error - API returns various formats:
84
84
  // "NotEnoughPoints", "insufficient", "not enough points" (lowercase)
85
- // Must check both error.message AND axios response data
86
- const errorMessage = error instanceof Error ? error.message : String(error);
85
+ // Must check error.message, data.message, AND data.errors array
87
86
  const axiosError = error;
88
- const responseMessage = axiosError.response?.data?.message || "";
89
- const combinedMessage = `${errorMessage} ${responseMessage}`.toLowerCase();
90
- if (combinedMessage.includes("notenoughpoints") || combinedMessage.includes("insufficient") || combinedMessage.includes("not enough points")) {
87
+ const apiErrors = axiosError.response?.data?.errors || [];
88
+ const allMessages = [
89
+ error instanceof Error ? error.message : String(error),
90
+ axiosError.response?.data?.message || "",
91
+ ...apiErrors.map(e => e.message)
92
+ ].join(" ").toLowerCase();
93
+ if (allMessages.includes("notenoughpoints") || allMessages.includes("insufficient") || allMessages.includes("not enough points")) {
91
94
  throw new OpenLoyaltyError({
92
95
  code: "INSUFFICIENT_BALANCE",
93
96
  message: "Member does not have enough points to complete this operation",
@@ -116,12 +119,15 @@ export async function pointsTransfer(input) {
116
119
  }
117
120
  catch (error) {
118
121
  // Check for insufficient points error - API returns various formats
119
- // Must check both error.message AND axios response data
120
- const errorMessage = error instanceof Error ? error.message : String(error);
122
+ // Must check error.message, data.message, AND data.errors array
121
123
  const axiosError = error;
122
- const responseMessage = axiosError.response?.data?.message || "";
123
- const combinedMessage = `${errorMessage} ${responseMessage}`.toLowerCase();
124
- if (combinedMessage.includes("notenoughpoints") || combinedMessage.includes("insufficient") || combinedMessage.includes("not enough points")) {
124
+ const apiErrors = axiosError.response?.data?.errors || [];
125
+ const allMessages = [
126
+ error instanceof Error ? error.message : String(error),
127
+ axiosError.response?.data?.message || "",
128
+ ...apiErrors.map(e => e.message)
129
+ ].join(" ").toLowerCase();
130
+ if (allMessages.includes("notenoughpoints") || allMessages.includes("insufficient") || allMessages.includes("not enough points")) {
125
131
  throw new OpenLoyaltyError({
126
132
  code: "INSUFFICIENT_BALANCE",
127
133
  message: "Sender does not have enough points to transfer",
@@ -35,9 +35,8 @@ export declare function rewardCreate(input: {
35
35
  from: string;
36
36
  to: string;
37
37
  };
38
- usageLimit: {
38
+ usageLimit?: {
39
39
  perUser: number;
40
- general?: number;
41
40
  };
42
41
  costInPoints?: number;
43
42
  usageInstruction?: string;
@@ -46,14 +46,19 @@ export async function rewardList(input) {
46
46
  }
47
47
  export async function rewardCreate(input) {
48
48
  const storeCode = getStoreCode(input.storeCode);
49
- // API requires: translations (name only), reward (type), activity, visibility, usageLimit.perUser
49
+ // API requires: translations (name only), reward (type), activity, visibility
50
+ // NOTE: usageLimit only accepts { perUser: N } - API rejects 'general' as extra field
50
51
  // NOTE: description is NOT supported by the API at creation time
52
+ // Sanitize usageLimit - ONLY pass perUser, strip any other fields (like 'general')
53
+ const sanitizedUsageLimit = input.usageLimit?.perUser !== undefined
54
+ ? { perUser: input.usageLimit.perUser }
55
+ : undefined;
51
56
  const payload = omitUndefined({
52
57
  translations: input.translations,
53
58
  reward: input.reward,
54
59
  activity: input.activity,
55
60
  visibility: input.visibility,
56
- usageLimit: input.usageLimit,
61
+ usageLimit: sanitizedUsageLimit,
57
62
  costInPoints: input.costInPoints,
58
63
  usageInstruction: input.usageInstruction,
59
64
  active: input.active,
@@ -93,12 +98,12 @@ export async function rewardUpdate(input) {
93
98
  const existing = await apiGet(`/${storeCode}/reward/${input.rewardId}`);
94
99
  // Extract only the fields that PUT accepts (API is strict about extra fields)
95
100
  // Note: GET returns name at root level, but PUT expects it in translations.en.name
101
+ // Note: usageLimit is NOT supported by the API - it rejects it as "extra fields"
96
102
  const existingName = existing.name;
97
103
  const existingActivity = existing.activity;
98
104
  const existingVisibility = existing.visibility;
99
- const existingUsageLimit = existing.usageLimit;
100
105
  // Build payload with only accepted fields - NEVER include undefined values
101
- // Only include activity/visibility/usageLimit if they exist in GET response
106
+ // Only include activity/visibility if they exist in GET response
102
107
  const payload = {
103
108
  // translations - only include name (GET returns name at root level)
104
109
  translations: {
@@ -132,12 +137,6 @@ export async function rewardUpdate(input) {
132
137
  visibilityPayload.to = existingVisibility.to;
133
138
  payload.visibility = visibilityPayload;
134
139
  }
135
- // usageLimit: BOTH perUser AND general are REQUIRED by the API
136
- // The GET response may include derived fields that PUT doesn't accept
137
- payload.usageLimit = {
138
- perUser: existingUsageLimit?.perUser ?? 1,
139
- general: existingUsageLimit?.general ?? 10000,
140
- };
141
140
  // Coupon-specific fields (required for static_coupon, dynamic_coupon, conversion_coupon)
142
141
  if (existing.couponValue !== undefined)
143
142
  payload.couponValue = existing.couponValue;
@@ -215,12 +214,15 @@ export async function rewardBuy(input) {
215
214
  catch (error) {
216
215
  // Check for insufficient points error - API returns various formats:
217
216
  // "NotEnoughPoints", "insufficient", "not enough points" (lowercase)
218
- // Must check both error.message AND axios response data
219
- const errorMessage = error instanceof Error ? error.message : String(error);
217
+ // Must check error.message, data.message, AND data.errors array
220
218
  const axiosError = error;
221
- const responseMessage = axiosError.response?.data?.message || "";
222
- const combinedMessage = `${errorMessage} ${responseMessage}`.toLowerCase();
223
- if (combinedMessage.includes("notenoughpoints") || combinedMessage.includes("insufficient") || combinedMessage.includes("not enough points")) {
219
+ const apiErrors = axiosError.response?.data?.errors || [];
220
+ const allMessages = [
221
+ error instanceof Error ? error.message : String(error),
222
+ axiosError.response?.data?.message || "",
223
+ ...apiErrors.map(e => e.message)
224
+ ].join(" ").toLowerCase();
225
+ if (allMessages.includes("notenoughpoints") || allMessages.includes("insufficient") || allMessages.includes("not enough points")) {
224
226
  throw new OpenLoyaltyError({
225
227
  code: "INSUFFICIENT_BALANCE",
226
228
  message: "Member does not have enough points to purchase this reward",
@@ -68,16 +68,13 @@ export declare const rewardToolDefinitions: readonly [{
68
68
  from: string;
69
69
  to: string;
70
70
  }>;
71
- usageLimit: import("zod").ZodObject<{
71
+ usageLimit: import("zod").ZodOptional<import("zod").ZodObject<{
72
72
  perUser: import("zod").ZodNumber;
73
- general: import("zod").ZodNumber;
74
- }, "strip", import("zod").ZodTypeAny, {
73
+ }, "strict", import("zod").ZodTypeAny, {
75
74
  perUser: number;
76
- general: number;
77
75
  }, {
78
76
  perUser: number;
79
- general: number;
80
- }>;
77
+ }>>;
81
78
  costInPoints: import("zod").ZodOptional<import("zod").ZodNumber>;
82
79
  usageInstruction: import("zod").ZodOptional<import("zod").ZodString>;
83
80
  active: import("zod").ZodOptional<import("zod").ZodBoolean>;
@@ -24,9 +24,9 @@ export const rewardToolDefinitions = [
24
24
  "1. translations: { en: { name: 'Reward Name' } }, " +
25
25
  "2. reward: 'material' or 'static_coupon' or 'dynamic_coupon', " +
26
26
  "3. activity: { from: '2026-01-01 00:00', to: '2027-12-31 23:59' }, " +
27
- "4. visibility: { from: '2026-01-01 00:00', to: '2027-12-31 23:59' }, " +
28
- "5. usageLimit: { perUser: 5, general: 1000 } - BOTH perUser AND general are REQUIRED. " +
27
+ "4. visibility: { from: '2026-01-01 00:00', to: '2027-12-31 23:59' }. " +
29
28
  "Types: material (physical goods), static_coupon (fixed discount), dynamic_coupon (variable value). " +
29
+ "For coupons: add couponValue, couponValueType ('money' or 'percentage'), daysValid. " +
30
30
  "For tier targeting: set target='level' and levels=['tier-uuid-1', 'tier-uuid-2'].",
31
31
  readOnly: false,
32
32
  inputSchema: RewardCreateInputSchema,
@@ -55,16 +55,13 @@ export declare const RewardCreateInputSchema: {
55
55
  from: string;
56
56
  to: string;
57
57
  }>;
58
- usageLimit: z.ZodObject<{
58
+ usageLimit: z.ZodOptional<z.ZodObject<{
59
59
  perUser: z.ZodNumber;
60
- general: z.ZodNumber;
61
- }, "strip", z.ZodTypeAny, {
60
+ }, "strict", z.ZodTypeAny, {
62
61
  perUser: number;
63
- general: number;
64
62
  }, {
65
63
  perUser: number;
66
- general: number;
67
- }>;
64
+ }>>;
68
65
  costInPoints: z.ZodOptional<z.ZodNumber>;
69
66
  usageInstruction: z.ZodOptional<z.ZodString>;
70
67
  active: z.ZodOptional<z.ZodBoolean>;
@@ -26,13 +26,11 @@ const RewardVisibilityInputSchema = z.object({
26
26
  from: z.string().describe("Visibility start datetime (format: 'YYYY-MM-DD HH:mm'). REQUIRED."),
27
27
  to: z.string().describe("Visibility end datetime (format: 'YYYY-MM-DD HH:mm'). REQUIRED."),
28
28
  });
29
- // Usage limit schema (REQUIRED by API)
30
- // NOTE: API uses 'general' (not 'total') for total redemptions limit
31
- // BOTH perUser and general are REQUIRED by the API
29
+ // Usage limit schema - API only accepts perUser (NOT general)
30
+ // Using .strict() to reject extra fields with clear error
32
31
  const RewardUsageLimitInputSchema = z.object({
33
- perUser: z.number().describe("Maximum redemptions per member. REQUIRED."),
34
- general: z.number().describe("Total redemptions across all members. REQUIRED. Use a large number (e.g., 10000) if unlimited."),
35
- });
32
+ perUser: z.number().describe("Maximum redemptions per member. This is the ONLY supported field."),
33
+ }).strict().describe("Usage limits. ⚠️ ONLY 'perUser' is supported. Do NOT pass 'general' or any other field - they will be rejected.");
36
34
  export const RewardCreateInputSchema = {
37
35
  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."),
38
36
  translations: RewardTranslationsInputSchema.describe("Reward name translations. At least 'en' key with { name } is REQUIRED."),
@@ -40,7 +38,7 @@ export const RewardCreateInputSchema = {
40
38
  "conversion_coupon (converts points to coupon), material (physical goods), fortune_wheel (gamified)."),
41
39
  activity: RewardActivityInputSchema.describe("Activity period when reward can be purchased. Use datetime format 'YYYY-MM-DD HH:mm'. REQUIRED."),
42
40
  visibility: RewardVisibilityInputSchema.describe("Visibility period when reward is shown. Use datetime format 'YYYY-MM-DD HH:mm'. REQUIRED."),
43
- usageLimit: RewardUsageLimitInputSchema.describe("Usage limits. ⚠️ BOTH perUser AND general are REQUIRED by the API."),
41
+ usageLimit: RewardUsageLimitInputSchema.optional().describe("Usage limits. Only perUser is supported by API."),
44
42
  costInPoints: z.number().optional().describe("Points required to redeem this reward."),
45
43
  usageInstruction: z.string().optional().describe("Instructions for using the reward."),
46
44
  active: z.boolean().optional().describe("Whether reward is active (default: false)."),
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { apiGet, apiPost } from "../client/http.js";
3
- import { formatApiError } from "../utils/errors.js";
3
+ import { formatApiError, OpenLoyaltyError } from "../utils/errors.js";
4
4
  import { getStoreCode } from "../config.js";
5
5
  import { buildPaginationParams, normalizeDateToISO } from "../utils/pagination.js";
6
6
  // Input Schemas
@@ -95,6 +95,19 @@ export async function transactionCreate(input) {
95
95
  };
96
96
  }
97
97
  catch (error) {
98
+ // Check for duplicate document number error
99
+ // Must check axios response data, not just error.message
100
+ const axiosError = error;
101
+ const apiErrors = axiosError.response?.data?.errors || [];
102
+ const allErrorMessages = apiErrors.map(e => e.message).join(" ").toLowerCase();
103
+ if (allErrorMessages.includes("document number") && allErrorMessages.includes("unique")) {
104
+ throw new OpenLoyaltyError({
105
+ code: "DUPLICATE_DOCUMENT",
106
+ message: `Transaction with document number '${input.header.documentNumber}' already exists`,
107
+ hint: `Each transaction must have a unique documentNumber. Use transaction_list(documentNumber: "${input.header.documentNumber}") to check if it exists, or generate a new unique document number (e.g., add timestamp suffix).`,
108
+ relatedTool: "ol_transaction_create",
109
+ });
110
+ }
98
111
  throw formatApiError(error, "ol_transaction_create");
99
112
  }
100
113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-loyalty/mcp-server",
3
- "version": "1.3.4",
3
+ "version": "1.3.6",
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>",