@open-loyalty/mcp-server 1.3.3 → 1.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tools/campaign/index.d.ts +5 -5
- package/dist/tools/campaign/index.js +4 -2
- package/dist/tools/campaign/schemas.d.ts +5 -5
- package/dist/tools/campaign/schemas.js +16 -3
- package/dist/tools/points.js +21 -5
- package/dist/tools/reward/handlers.d.ts +1 -2
- package/dist/tools/reward/handlers.js +15 -10
- package/dist/tools/reward/index.d.ts +2 -5
- package/dist/tools/reward/index.js +2 -2
- package/dist/tools/reward/schemas.d.ts +2 -5
- package/dist/tools/reward/schemas.js +4 -7
- package/dist/tools/tierset.d.ts +0 -27
- package/dist/tools/tierset.js +27 -20
- package/dist/tools/transaction.js +14 -1
- package/package.json +1 -1
|
@@ -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").
|
|
103
|
+
data: import("zod").ZodUnknown;
|
|
104
104
|
}, "strip", import("zod").ZodTypeAny, {
|
|
105
105
|
attribute: string;
|
|
106
106
|
operator: string;
|
|
107
|
-
data?:
|
|
107
|
+
data?: unknown;
|
|
108
108
|
}, {
|
|
109
109
|
attribute: string;
|
|
110
110
|
operator: string;
|
|
111
|
-
data?:
|
|
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?:
|
|
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?:
|
|
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
|
|
31
|
-
"
|
|
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.
|
|
498
|
+
data: z.ZodUnknown;
|
|
499
499
|
}, "strip", z.ZodTypeAny, {
|
|
500
500
|
attribute: string;
|
|
501
501
|
operator: string;
|
|
502
|
-
data?:
|
|
502
|
+
data?: unknown;
|
|
503
503
|
}, {
|
|
504
504
|
attribute: string;
|
|
505
505
|
operator: string;
|
|
506
|
-
data?:
|
|
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?:
|
|
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?:
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
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({
|
package/dist/tools/points.js
CHANGED
|
@@ -80,9 +80,17 @@ export async function pointsSpend(input) {
|
|
|
80
80
|
return { transferId: response.transferId };
|
|
81
81
|
}
|
|
82
82
|
catch (error) {
|
|
83
|
-
// Check for insufficient points error
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
// Check for insufficient points error - API returns various formats:
|
|
84
|
+
// "NotEnoughPoints", "insufficient", "not enough points" (lowercase)
|
|
85
|
+
// Must check error.message, data.message, AND data.errors array
|
|
86
|
+
const axiosError = error;
|
|
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")) {
|
|
86
94
|
throw new OpenLoyaltyError({
|
|
87
95
|
code: "INSUFFICIENT_BALANCE",
|
|
88
96
|
message: "Member does not have enough points to complete this operation",
|
|
@@ -110,8 +118,16 @@ export async function pointsTransfer(input) {
|
|
|
110
118
|
return { transferId: response.transferId };
|
|
111
119
|
}
|
|
112
120
|
catch (error) {
|
|
113
|
-
|
|
114
|
-
|
|
121
|
+
// Check for insufficient points error - API returns various formats
|
|
122
|
+
// Must check error.message, data.message, AND data.errors array
|
|
123
|
+
const axiosError = error;
|
|
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")) {
|
|
115
131
|
throw new OpenLoyaltyError({
|
|
116
132
|
code: "INSUFFICIENT_BALANCE",
|
|
117
133
|
message: "Sender does not have enough points to transfer",
|
|
@@ -46,7 +46,8 @@ 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
|
|
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
|
|
51
52
|
const payload = omitUndefined({
|
|
52
53
|
translations: input.translations,
|
|
@@ -93,12 +94,12 @@ export async function rewardUpdate(input) {
|
|
|
93
94
|
const existing = await apiGet(`/${storeCode}/reward/${input.rewardId}`);
|
|
94
95
|
// Extract only the fields that PUT accepts (API is strict about extra fields)
|
|
95
96
|
// Note: GET returns name at root level, but PUT expects it in translations.en.name
|
|
97
|
+
// Note: usageLimit is NOT supported by the API - it rejects it as "extra fields"
|
|
96
98
|
const existingName = existing.name;
|
|
97
99
|
const existingActivity = existing.activity;
|
|
98
100
|
const existingVisibility = existing.visibility;
|
|
99
|
-
const existingUsageLimit = existing.usageLimit;
|
|
100
101
|
// Build payload with only accepted fields - NEVER include undefined values
|
|
101
|
-
// Only include activity/visibility
|
|
102
|
+
// Only include activity/visibility if they exist in GET response
|
|
102
103
|
const payload = {
|
|
103
104
|
// translations - only include name (GET returns name at root level)
|
|
104
105
|
translations: {
|
|
@@ -132,11 +133,6 @@ export async function rewardUpdate(input) {
|
|
|
132
133
|
visibilityPayload.to = existingVisibility.to;
|
|
133
134
|
payload.visibility = visibilityPayload;
|
|
134
135
|
}
|
|
135
|
-
// usageLimit: Required field, but only include perUser (API rejects extra fields)
|
|
136
|
-
// The GET response may include derived fields that PUT doesn't accept
|
|
137
|
-
payload.usageLimit = {
|
|
138
|
-
perUser: existingUsageLimit?.perUser ?? 1,
|
|
139
|
-
};
|
|
140
136
|
// Coupon-specific fields (required for static_coupon, dynamic_coupon, conversion_coupon)
|
|
141
137
|
if (existing.couponValue !== undefined)
|
|
142
138
|
payload.couponValue = existing.couponValue;
|
|
@@ -212,8 +208,17 @@ export async function rewardBuy(input) {
|
|
|
212
208
|
};
|
|
213
209
|
}
|
|
214
210
|
catch (error) {
|
|
215
|
-
|
|
216
|
-
|
|
211
|
+
// Check for insufficient points error - API returns various formats:
|
|
212
|
+
// "NotEnoughPoints", "insufficient", "not enough points" (lowercase)
|
|
213
|
+
// Must check error.message, data.message, AND data.errors array
|
|
214
|
+
const axiosError = error;
|
|
215
|
+
const apiErrors = axiosError.response?.data?.errors || [];
|
|
216
|
+
const allMessages = [
|
|
217
|
+
error instanceof Error ? error.message : String(error),
|
|
218
|
+
axiosError.response?.data?.message || "",
|
|
219
|
+
...apiErrors.map(e => e.message)
|
|
220
|
+
].join(" ").toLowerCase();
|
|
221
|
+
if (allMessages.includes("notenoughpoints") || allMessages.includes("insufficient") || allMessages.includes("not enough points")) {
|
|
217
222
|
throw new OpenLoyaltyError({
|
|
218
223
|
code: "INSUFFICIENT_BALANCE",
|
|
219
224
|
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
73
|
}, "strip", import("zod").ZodTypeAny, {
|
|
75
74
|
perUser: number;
|
|
76
|
-
general: number;
|
|
77
75
|
}, {
|
|
78
76
|
perUser: number;
|
|
79
|
-
|
|
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
60
|
}, "strip", z.ZodTypeAny, {
|
|
62
61
|
perUser: number;
|
|
63
|
-
general: number;
|
|
64
62
|
}, {
|
|
65
63
|
perUser: number;
|
|
66
|
-
|
|
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,10 @@ 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 (
|
|
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)
|
|
32
30
|
const RewardUsageLimitInputSchema = z.object({
|
|
33
|
-
perUser: z.number().describe("Maximum redemptions per member.
|
|
34
|
-
|
|
35
|
-
});
|
|
31
|
+
perUser: z.number().describe("Maximum redemptions per member."),
|
|
32
|
+
}).describe("Usage limits. ⚠️ Only 'perUser' is supported - API rejects 'general' as extra field.");
|
|
36
33
|
export const RewardCreateInputSchema = {
|
|
37
34
|
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
35
|
translations: RewardTranslationsInputSchema.describe("Reward name translations. At least 'en' key with { name } is REQUIRED."),
|
|
@@ -40,7 +37,7 @@ export const RewardCreateInputSchema = {
|
|
|
40
37
|
"conversion_coupon (converts points to coupon), material (physical goods), fortune_wheel (gamified)."),
|
|
41
38
|
activity: RewardActivityInputSchema.describe("Activity period when reward can be purchased. Use datetime format 'YYYY-MM-DD HH:mm'. REQUIRED."),
|
|
42
39
|
visibility: RewardVisibilityInputSchema.describe("Visibility period when reward is shown. Use datetime format 'YYYY-MM-DD HH:mm'. REQUIRED."),
|
|
43
|
-
usageLimit: RewardUsageLimitInputSchema.describe("Usage limits.
|
|
40
|
+
usageLimit: RewardUsageLimitInputSchema.optional().describe("Usage limits. Only perUser is supported by API."),
|
|
44
41
|
costInPoints: z.number().optional().describe("Points required to redeem this reward."),
|
|
45
42
|
usageInstruction: z.string().optional().describe("Instructions for using the reward."),
|
|
46
43
|
active: z.boolean().optional().describe("Whether reward is active (default: false)."),
|
package/dist/tools/tierset.d.ts
CHANGED
|
@@ -19,17 +19,6 @@ export declare const TierSetCreateInputSchema: {
|
|
|
19
19
|
attribute: "activeUnits" | "totalEarnedUnits" | "totalSpending" | "monthsSinceJoiningProgram" | "cumulatedEarnedUnits";
|
|
20
20
|
walletType?: string | undefined;
|
|
21
21
|
}>, "many">;
|
|
22
|
-
downgrade: z.ZodOptional<z.ZodObject<{
|
|
23
|
-
mode: z.ZodEnum<["none", "automatic", "x_days"]>;
|
|
24
|
-
days: z.ZodOptional<z.ZodNumber>;
|
|
25
|
-
}, "strip", z.ZodTypeAny, {
|
|
26
|
-
mode: "none" | "automatic" | "x_days";
|
|
27
|
-
days?: number | undefined;
|
|
28
|
-
}, {
|
|
29
|
-
mode: "none" | "automatic" | "x_days";
|
|
30
|
-
days?: number | undefined;
|
|
31
|
-
}>>;
|
|
32
|
-
active: z.ZodOptional<z.ZodBoolean>;
|
|
33
22
|
};
|
|
34
23
|
export declare const TierSetGetInputSchema: {
|
|
35
24
|
storeCode: z.ZodOptional<z.ZodString>;
|
|
@@ -107,11 +96,6 @@ type TierSetCreateInput = {
|
|
|
107
96
|
attribute: string;
|
|
108
97
|
walletType?: string;
|
|
109
98
|
}[];
|
|
110
|
-
downgrade?: {
|
|
111
|
-
mode: string;
|
|
112
|
-
days?: number;
|
|
113
|
-
};
|
|
114
|
-
active?: boolean;
|
|
115
99
|
};
|
|
116
100
|
type TierSetGetInput = {
|
|
117
101
|
storeCode?: string;
|
|
@@ -196,17 +180,6 @@ export declare const tiersetToolDefinitions: readonly [{
|
|
|
196
180
|
attribute: "activeUnits" | "totalEarnedUnits" | "totalSpending" | "monthsSinceJoiningProgram" | "cumulatedEarnedUnits";
|
|
197
181
|
walletType?: string | undefined;
|
|
198
182
|
}>, "many">;
|
|
199
|
-
downgrade: z.ZodOptional<z.ZodObject<{
|
|
200
|
-
mode: z.ZodEnum<["none", "automatic", "x_days"]>;
|
|
201
|
-
days: z.ZodOptional<z.ZodNumber>;
|
|
202
|
-
}, "strip", z.ZodTypeAny, {
|
|
203
|
-
mode: "none" | "automatic" | "x_days";
|
|
204
|
-
days?: number | undefined;
|
|
205
|
-
}, {
|
|
206
|
-
mode: "none" | "automatic" | "x_days";
|
|
207
|
-
days?: number | undefined;
|
|
208
|
-
}>>;
|
|
209
|
-
active: z.ZodOptional<z.ZodBoolean>;
|
|
210
183
|
};
|
|
211
184
|
readonly handler: typeof tiersetCreate;
|
|
212
185
|
}, {
|
package/dist/tools/tierset.js
CHANGED
|
@@ -20,11 +20,7 @@ export const TierSetCreateInputSchema = {
|
|
|
20
20
|
walletType: z.string().optional().describe("Wallet type CODE (not UUID). Required for unit-based attributes (activeUnits, totalEarnedUnits, cumulatedEarnedUnits). " +
|
|
21
21
|
"Use wallet_type_list to find walletType.code (e.g., 'default')."),
|
|
22
22
|
})).describe("Array of conditions that define tier progression criteria. IMPORTANT: Use 'totalEarnedUnits' for lifetime points (NOT 'earnedUnits')."),
|
|
23
|
-
|
|
24
|
-
mode: DowngradeModeEnum.describe("Downgrade mode."),
|
|
25
|
-
days: z.number().optional().describe("Number of days for x_days mode (required if mode is x_days)."),
|
|
26
|
-
}).optional().describe("Downgrade configuration for the tier set."),
|
|
27
|
-
active: z.boolean().optional().describe("Whether the tier set is active. Defaults to true."),
|
|
23
|
+
// NOTE: downgrade and active are NOT supported at creation time - use tierset_update after creation
|
|
28
24
|
};
|
|
29
25
|
export const TierSetGetInputSchema = {
|
|
30
26
|
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."),
|
|
@@ -87,25 +83,34 @@ export async function tiersetList(input) {
|
|
|
87
83
|
}
|
|
88
84
|
export async function tiersetCreate(input) {
|
|
89
85
|
const storeCode = getStoreCode(input.storeCode);
|
|
90
|
-
// API
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
86
|
+
// API QUIRK: Only accepts specific fields at creation time.
|
|
87
|
+
// Fields NOT supported at creation: tiers, active, downgrade
|
|
88
|
+
// These must be set via tierset_update or tierset_update_tiers after creation.
|
|
89
|
+
//
|
|
90
|
+
// REQUIRED fields:
|
|
91
|
+
// - translations.en.name (string)
|
|
92
|
+
// - conditions (array with attribute, optionally walletType)
|
|
93
|
+
const tierSetPayload = {
|
|
94
|
+
translations: {
|
|
95
|
+
en: {
|
|
96
|
+
name: input.name,
|
|
97
|
+
description: input.description || "",
|
|
99
98
|
},
|
|
100
|
-
conditions: input.conditions,
|
|
101
|
-
tiers: [], // Required by API - tiers are added separately via tierset_update_tiers
|
|
102
|
-
downgrade: input.downgrade,
|
|
103
|
-
active: input.active ?? true,
|
|
104
99
|
},
|
|
100
|
+
conditions: input.conditions,
|
|
105
101
|
};
|
|
102
|
+
const payload = { tierSet: tierSetPayload };
|
|
106
103
|
try {
|
|
107
104
|
const response = await apiPost(`/${storeCode}/tierSet`, payload);
|
|
108
|
-
|
|
105
|
+
// API QUIRK: Create endpoint only returns { tierSetId: string }
|
|
106
|
+
// We need to fetch the full tier set to get the conditionId values
|
|
107
|
+
const createResponse = response;
|
|
108
|
+
if (!createResponse.tierSetId) {
|
|
109
|
+
throw new Error(`Unexpected response format: ${JSON.stringify(response)}`);
|
|
110
|
+
}
|
|
111
|
+
// Fetch the created tier set to get condition IDs
|
|
112
|
+
const tierSetResponse = await apiGet(`/${storeCode}/tierSet/${createResponse.tierSetId}`);
|
|
113
|
+
const validated = TierSetSchema.parse(tierSetResponse);
|
|
109
114
|
return {
|
|
110
115
|
tierSetId: validated.tierSetId,
|
|
111
116
|
conditions: validated.conditions.map((c) => ({
|
|
@@ -228,7 +233,9 @@ export const tiersetToolDefinitions = [
|
|
|
228
233
|
description: "Create a new tier set (loyalty program structure). " +
|
|
229
234
|
"⚠️ LIMIT: Maximum 3 ACTIVE tier sets per store. ALWAYS call tierset_list FIRST to check existing tier sets - if one exists that matches your needs, REUSE IT instead of creating a new one. " +
|
|
230
235
|
"WORKFLOW: 1) tierset_list (check existing) → 2) tierset_create (only if needed) → 3) tierset_get (to get conditionId) → 4) tierset_update_tiers (to define thresholds). " +
|
|
231
|
-
"
|
|
236
|
+
"⚠️ API LIMITATION: Only 'name', 'description', and 'conditions' are accepted at creation time. " +
|
|
237
|
+
"Fields like 'active', 'downgrade', 'tiers' are NOT supported at creation - use tierset_update after creation to set these. " +
|
|
238
|
+
"Valid condition attributes: 'totalEarnedUnits' (lifetime points), 'activeUnits' (current balance), 'totalSpending', 'monthsSinceJoiningProgram'. " +
|
|
232
239
|
"COMMON MISTAKE: Use 'totalEarnedUnits' NOT 'earnedUnits' for lifetime points. " +
|
|
233
240
|
"For unit-based attributes, set walletType to 'default'.",
|
|
234
241
|
readOnly: 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.
|
|
3
|
+
"version": "1.3.5",
|
|
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>",
|