@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.
- 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 +16 -10
- package/dist/tools/reward/handlers.d.ts +1 -2
- package/dist/tools/reward/handlers.js +17 -15
- package/dist/tools/reward/index.d.ts +3 -6
- package/dist/tools/reward/index.js +2 -2
- package/dist/tools/reward/schemas.d.ts +3 -6
- package/dist/tools/reward/schemas.js +5 -7
- 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
|
@@ -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
|
|
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
|
|
89
|
-
const
|
|
90
|
-
|
|
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
|
|
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
|
|
123
|
-
const
|
|
124
|
-
|
|
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",
|
|
@@ -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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
222
|
-
const
|
|
223
|
-
|
|
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
|
-
|
|
74
|
-
}, "strip", import("zod").ZodTypeAny, {
|
|
73
|
+
}, "strict", 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
|
-
|
|
61
|
-
}, "strip", z.ZodTypeAny, {
|
|
60
|
+
}, "strict", 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,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 (
|
|
30
|
-
//
|
|
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.
|
|
34
|
-
|
|
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.
|
|
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.
|
|
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>",
|