@sendinel/mcp-server 1.0.149 → 1.0.151
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/build/tools/campaigns.js +37 -6
- package/build/tools/delivery-ops.js +3 -3
- package/build/tools/gdpr.js +1 -1
- package/build/tools/newsletters.js +3 -3
- package/build/tools/short-links.js +5 -3
- package/build/tools/surveys.js +2 -2
- package/build/tools/templates.js +1 -1
- package/package.json +1 -1
package/build/tools/campaigns.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateStepMultiEventRules, stepMultiEventRulesView } from "@sendinel/domain";
|
|
1
2
|
import { readCampaignMetricGroups, SENT_STATUSES } from "@sendinel/domain";
|
|
2
3
|
import { resolveCampaignReentry, readCampaignPeriods, campaignPeriodFields } from "@sendinel/domain";
|
|
3
4
|
import { CAMPAIGN_BRANCH_CONDITIONS, campaignBranchConditionValue, isContactPropertyConditionList, STEP_PROPERTY_CONDITIONS_ERROR, validateStepPropertyRules } from "@sendinel/domain";
|
|
@@ -40,6 +41,12 @@ const reentryFields = {
|
|
|
40
41
|
reentry_mode: z.enum(['once_ever', 'per_event_period']).optional().describe(REENTRY_DESCRIPTION),
|
|
41
42
|
period_key: z.string().min(1).max(64).nullable().optional().describe('Flat event-data key naming the period (starts with a letter; letters, digits and underscores only). Key alone selects per_event_period; null clears to once ever. ' + REENTRY_DESCRIPTION),
|
|
42
43
|
};
|
|
44
|
+
// Schema rejects empty/oversized writes; shared validation trims and deduplicates before storage.
|
|
45
|
+
const multiEventFields = {
|
|
46
|
+
skip_if_events: z.array(z.string().trim().min(1)).min(1).max(10).nullable().optional().describe('Skip if ANY listed event has occurred. ANDed with legacy and required gates. null clears.'),
|
|
47
|
+
send_only_if_events: z.array(z.string().trim().min(1)).min(1).max(10).nullable().optional().describe('Required events at send time, ALL by default; null clears. ANDed with legacy gates.'),
|
|
48
|
+
send_only_if_events_mode: z.enum(['all', 'any']).nullable().optional().describe('Required-event list mode; omitted/null defaults to all on new steps. Omission preserves the mode on updates.'),
|
|
49
|
+
};
|
|
43
50
|
function stripHtml(value) {
|
|
44
51
|
return value
|
|
45
52
|
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
@@ -891,11 +898,15 @@ export function registerCampaignTools(server) {
|
|
|
891
898
|
brand_asset_ids: z.array(z.string().uuid()).optional().describe('Stable brand_media_assets ids explicitly referenced by this step.'),
|
|
892
899
|
skip_if_property: propertyConditions.describe('Skip when ALL 1–10 conditions match current custom_properties. Missing/null keys never match. null clears.'),
|
|
893
900
|
send_only_if_property: propertyConditions.describe('Send only when ALL 1–10 conditions match current custom_properties. Unknown/non-match skips. null clears.'),
|
|
901
|
+
...multiEventFields,
|
|
894
902
|
skip_if_event: z.string().trim().min(1).optional().describe('Skip this step at send time if the contact already has an event with this name; the journey continues.'),
|
|
895
903
|
send_only_if_event: z.string().trim().min(1).optional().describe('Send this step only if the contact has an event with this name by send time; otherwise skip it and continue the journey.'),
|
|
896
904
|
},
|
|
897
|
-
}, async ({ campaign_id, step_order, subject, body_html, body_text, blocks, delay_days, delay_hours, variant_label, survey_id, brand_asset_ids, skip_if_event, send_only_if_event, skip_if_property, send_only_if_property }) => {
|
|
905
|
+
}, async ({ campaign_id, step_order, subject, body_html, body_text, blocks, delay_days, delay_hours, variant_label, survey_id, brand_asset_ids, skip_if_event, send_only_if_event, skip_if_events, send_only_if_events, send_only_if_events_mode, skip_if_property, send_only_if_property }) => {
|
|
898
906
|
try {
|
|
907
|
+
const events = validateStepMultiEventRules({ skip_if_events, send_only_if_events, send_only_if_events_mode });
|
|
908
|
+
if (!events.ok)
|
|
909
|
+
return j({ error: events.error, code: 'INVALID_INPUT' });
|
|
899
910
|
const properties = validateStepPropertyRules({ skip_if_property, send_only_if_property });
|
|
900
911
|
if (!properties.ok)
|
|
901
912
|
return j({ error: properties.error, code: "INVALID_INPUT" });
|
|
@@ -932,7 +943,7 @@ export function registerCampaignTools(server) {
|
|
|
932
943
|
const renderedText = body_text !== undefined && survey_id ? renderTemplateVars(body_text, renderVars) : body_text;
|
|
933
944
|
const { data, error } = await db()
|
|
934
945
|
.from('campaign_steps')
|
|
935
|
-
.insert({ campaign_id, step_order, subject: renderedSubject, body_html: renderedHtml, body_text: renderedText ?? (normalizedBlocks?.length ? blocksToText(normalizedBlocks) : undefined), blocks: normalizedBlocks ?? null, delay_days, delay_hours, variant_label: variant_label ?? 'A', skip_if_event: skip_if_event ?? null, send_only_if_event: send_only_if_event ?? null, ...properties.rules })
|
|
946
|
+
.insert({ campaign_id, step_order, subject: renderedSubject, body_html: renderedHtml, body_text: renderedText ?? (normalizedBlocks?.length ? blocksToText(normalizedBlocks) : undefined), blocks: normalizedBlocks ?? null, delay_days, delay_hours, variant_label: variant_label ?? 'A', skip_if_event: skip_if_event ?? null, send_only_if_event: send_only_if_event ?? null, ...events.rules, ...properties.rules })
|
|
936
947
|
.select()
|
|
937
948
|
.single();
|
|
938
949
|
if (error)
|
|
@@ -2936,7 +2947,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
|
|
|
2936
2947
|
: row.status === 'expired' ? 'no_reply_by_deadline'
|
|
2937
2948
|
: 'unknown',
|
|
2938
2949
|
}));
|
|
2939
|
-
return j({ items, count: items.length });
|
|
2950
|
+
return j({ items, count: items.length, truncated: items.length === limit });
|
|
2940
2951
|
});
|
|
2941
2952
|
server.registerTool('validate_template', {
|
|
2942
2953
|
title: 'Validate Template',
|
|
@@ -3112,6 +3123,9 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3112
3123
|
trigger_type: z.enum(['manual', 'date_field', 'contact_event']).nullable().optional(),
|
|
3113
3124
|
trigger_config: z.record(z.string(), z.unknown()).nullable().optional().describe('Event/tag lists with optional where filter, or date_field/contact_event configuration.'),
|
|
3114
3125
|
steps: z.array(z.object({
|
|
3126
|
+
...multiEventFields,
|
|
3127
|
+
skip_if_event: z.string().trim().min(1).nullable().optional(),
|
|
3128
|
+
send_only_if_event: z.string().trim().min(1).nullable().optional(),
|
|
3115
3129
|
subject: z.string().min(1).describe('Email subject line'),
|
|
3116
3130
|
body_html: z.string().min(1).describe('Full HTML email body'),
|
|
3117
3131
|
body_text: z.string().optional().describe('Plain text fallback'),
|
|
@@ -3126,6 +3140,12 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3126
3140
|
approval_id: z.string().uuid().optional().describe('Approval token from a previous call. Required when activate=true — get this by calling once without it, then have a human approve via the dashboard.'),
|
|
3127
3141
|
},
|
|
3128
3142
|
}, async ({ site_id, name, type, steps, segment_id, sends_per_hour, activate, confirmed, approval_id, trigger_event_name, trigger_tag, trigger_type, trigger_config, send_class, unsub_footer_template, reentry_mode, period_key }) => {
|
|
3143
|
+
for (const step of steps) {
|
|
3144
|
+
const events = validateStepMultiEventRules(step);
|
|
3145
|
+
if (!events.ok)
|
|
3146
|
+
return j({ error: events.error, code: 'INVALID_INPUT' });
|
|
3147
|
+
Object.assign(step, events.rules);
|
|
3148
|
+
}
|
|
3129
3149
|
const footerTemplate = unsub_footer_template?.trim() ?? null;
|
|
3130
3150
|
if (footerTemplate !== null) {
|
|
3131
3151
|
const footerError = validateUnsubFooterTemplate(footerTemplate);
|
|
@@ -3202,6 +3222,9 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3202
3222
|
delay: s.delay_days ? `${s.delay_days}d ${s.delay_hours ?? 0}h` : `${s.delay_hours ?? 0}h`,
|
|
3203
3223
|
has_html: !!s.body_html,
|
|
3204
3224
|
has_text: !!s.body_text,
|
|
3225
|
+
skip_if_event: s.skip_if_event ?? null,
|
|
3226
|
+
send_only_if_event: s.send_only_if_event ?? null,
|
|
3227
|
+
...stepMultiEventRulesView(s),
|
|
3205
3228
|
})),
|
|
3206
3229
|
segment_id: segment_id ?? null,
|
|
3207
3230
|
sends_per_hour: sends_per_hour ?? null,
|
|
@@ -3240,6 +3263,9 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3240
3263
|
blocks: s.blocks ? normalizeComposerBlocks(s.blocks) : null,
|
|
3241
3264
|
delay_days: s.delay_days ?? 0,
|
|
3242
3265
|
delay_hours: s.delay_hours ?? 0,
|
|
3266
|
+
skip_if_event: s.skip_if_event ?? null,
|
|
3267
|
+
send_only_if_event: s.send_only_if_event ?? null,
|
|
3268
|
+
...stepMultiEventRulesView(s),
|
|
3243
3269
|
}));
|
|
3244
3270
|
const { error: stepErr } = await db()
|
|
3245
3271
|
.from('campaign_steps')
|
|
@@ -3337,7 +3363,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3337
3363
|
return j({ error: (campaignErr ? dbToolError(campaignErr).error : undefined) ?? 'Campaign not found', code: 'NOT_FOUND' });
|
|
3338
3364
|
const { data, error } = await db()
|
|
3339
3365
|
.from('campaign_steps')
|
|
3340
|
-
.select('id, step_order, subject, body_html, body_text, blocks, delay_days, delay_hours, skip_if_event, send_only_if_event, skip_if_property, send_only_if_property, created_at, updated_at')
|
|
3366
|
+
.select('id, step_order, subject, body_html, body_text, blocks, delay_days, delay_hours, skip_if_event, send_only_if_event, skip_if_events, send_only_if_events, send_only_if_events_mode, skip_if_property, send_only_if_property, created_at, updated_at')
|
|
3341
3367
|
.eq('campaign_id', campaign_id)
|
|
3342
3368
|
.order('step_order', { ascending: true });
|
|
3343
3369
|
if (error)
|
|
@@ -3365,6 +3391,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3365
3391
|
survey_id: z.string().uuid().optional(),
|
|
3366
3392
|
skip_if_property: propertyConditions.describe('Skip when ALL 1–10 conditions match current custom_properties. Missing/null keys never match. null clears.'),
|
|
3367
3393
|
send_only_if_property: propertyConditions.describe('Send only when ALL 1–10 conditions match current custom_properties. Unknown/non-match skips. null clears.'),
|
|
3394
|
+
...multiEventFields,
|
|
3368
3395
|
skip_if_event: z.string().trim().min(1).nullable().optional().describe('Event name that skips this step at send time; null clears the rule.'),
|
|
3369
3396
|
send_only_if_event: z.string().trim().min(1).nullable().optional().describe('Event required for this step to send; null clears the rule.'),
|
|
3370
3397
|
link_actions: z.array(z.discriminatedUnion('action', [
|
|
@@ -3381,6 +3408,9 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3381
3408
|
}, async ({ step_id, ...updates }) => {
|
|
3382
3409
|
const projectId = getProjectId();
|
|
3383
3410
|
const { survey_id, ...stepUpdates } = updates;
|
|
3411
|
+
const events = validateStepMultiEventRules(stepUpdates);
|
|
3412
|
+
if (!events.ok)
|
|
3413
|
+
return j({ error: events.error, code: 'INVALID_INPUT' });
|
|
3384
3414
|
const properties = validateStepPropertyRules(stepUpdates);
|
|
3385
3415
|
if (!properties.ok)
|
|
3386
3416
|
return j({ error: properties.error, code: "INVALID_INPUT" });
|
|
@@ -3409,6 +3439,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3409
3439
|
const normalizedBlocks = Array.isArray(stepUpdates.blocks) ? normalizeComposerBlocks(stepUpdates.blocks) : undefined;
|
|
3410
3440
|
const renderedUpdates = {
|
|
3411
3441
|
...stepUpdates,
|
|
3442
|
+
...events.rules,
|
|
3412
3443
|
...(normalizedBlocks !== undefined ? { blocks: normalizedBlocks.length ? normalizedBlocks : null } : {}),
|
|
3413
3444
|
...(normalizedBlocks?.length && stepUpdates.body_html === undefined ? { body_html: blocksToHtml(normalizedBlocks) } : {}),
|
|
3414
3445
|
...(normalizedBlocks?.length && stepUpdates.body_text === undefined ? { body_text: blocksToText(normalizedBlocks) } : {}),
|
|
@@ -3420,7 +3451,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3420
3451
|
.from('campaign_steps')
|
|
3421
3452
|
.update({ ...renderedUpdates, updated_at: new Date().toISOString() })
|
|
3422
3453
|
.eq('id', step_id)
|
|
3423
|
-
.select('id, step_order, subject, delay_days, delay_hours, skip_if_event, send_only_if_event, skip_if_property, send_only_if_property, updated_at')
|
|
3454
|
+
.select('id, step_order, subject, delay_days, delay_hours, skip_if_event, send_only_if_event, skip_if_events, send_only_if_events, send_only_if_events_mode, skip_if_property, send_only_if_property, updated_at')
|
|
3424
3455
|
.single();
|
|
3425
3456
|
if (error)
|
|
3426
3457
|
return j(dbToolError(error));
|
|
@@ -3798,7 +3829,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3798
3829
|
total_sent: periods?.totals?.sent ?? sentCount ?? 0,
|
|
3799
3830
|
...(periods?.totals ? { unique_opened: periods.totals.opened, unique_clicked: periods.totals.clicked } : {}),
|
|
3800
3831
|
...campaignPeriodFields(periods, period),
|
|
3801
|
-
steps: (steps ?? []).map((step) => ({ ...step, total_sent: stepMetrics.sent.get(step.id) ?? 0 })),
|
|
3832
|
+
steps: (steps ?? []).map((step) => ({ ...step, ...stepMultiEventRulesView(step), total_sent: stepMetrics.sent.get(step.id) ?? 0 })),
|
|
3802
3833
|
active_enrollments: enrollCount ?? 0,
|
|
3803
3834
|
send_incomplete: queuedCount > 0 || failedCount > 0 || outstandingRows.length > 0 || unresolvedCount > 0,
|
|
3804
3835
|
queued_email_count: queuedCount,
|
|
@@ -341,7 +341,7 @@ export function registerDeliveryOpsTools(server) {
|
|
|
341
341
|
if (error)
|
|
342
342
|
return j(dbToolError(error));
|
|
343
343
|
const failed = (data ?? []).filter((row) => row.status === 'failed');
|
|
344
|
-
return j({ runs: data ?? [], recent_failures: failed.length, total: data?.length ?? 0 });
|
|
344
|
+
return j({ runs: data ?? [], recent_failures: failed.length, total: data?.length ?? 0, truncated: (data?.length ?? 0) === limit });
|
|
345
345
|
});
|
|
346
346
|
server.registerTool('get_queue_status', {
|
|
347
347
|
title: 'Get Queue Status',
|
|
@@ -406,7 +406,7 @@ export function registerDeliveryOpsTools(server) {
|
|
|
406
406
|
const { data, error } = await query;
|
|
407
407
|
if (error)
|
|
408
408
|
return j(dbToolError(error));
|
|
409
|
-
return j({ dlq: data ?? [], count: data?.length ?? 0 });
|
|
409
|
+
return j({ dlq: data ?? [], count: data?.length ?? 0, truncated: (data?.length ?? 0) === limit });
|
|
410
410
|
});
|
|
411
411
|
server.registerTool('get_enrollment_dlq', {
|
|
412
412
|
title: 'Get Enrollment DLQ',
|
|
@@ -436,7 +436,7 @@ export function registerDeliveryOpsTools(server) {
|
|
|
436
436
|
const { data, error } = await query;
|
|
437
437
|
if (error)
|
|
438
438
|
return j(dbToolError(error));
|
|
439
|
-
return j({ dlq: data ?? [], count: data?.length ?? 0 });
|
|
439
|
+
return j({ dlq: data ?? [], count: data?.length ?? 0, truncated: (data?.length ?? 0) === limit });
|
|
440
440
|
});
|
|
441
441
|
server.registerTool('get_send_queue_rows', {
|
|
442
442
|
title: 'Get Send Queue Rows',
|
package/build/tools/gdpr.js
CHANGED
|
@@ -110,7 +110,7 @@ export function registerGdprTools(server) {
|
|
|
110
110
|
.limit(limit ?? 50);
|
|
111
111
|
if (error)
|
|
112
112
|
return txt(`Error: ${dbToolError(error).error}`);
|
|
113
|
-
return j({ requests: data ?? [], count: data?.length ?? 0 });
|
|
113
|
+
return j({ requests: data ?? [], count: data?.length ?? 0, truncated: (data?.length ?? 0) === (limit ?? 50) });
|
|
114
114
|
});
|
|
115
115
|
server.registerTool('action_ccpa_request', {
|
|
116
116
|
title: 'Action CCPA Request',
|
|
@@ -183,7 +183,7 @@ export function registerNewsletterTools(server) {
|
|
|
183
183
|
const { data, error } = await query;
|
|
184
184
|
if (error)
|
|
185
185
|
return j(dbToolError(error));
|
|
186
|
-
return j({ newsletters: data ?? [], count: data?.length ?? 0 });
|
|
186
|
+
return j({ newsletters: data ?? [], count: data?.length ?? 0, truncated: (data?.length ?? 0) === limit });
|
|
187
187
|
});
|
|
188
188
|
server.registerTool('get_newsletter', {
|
|
189
189
|
title: 'Get Newsletter',
|
|
@@ -376,7 +376,7 @@ export function registerNewsletterTools(server) {
|
|
|
376
376
|
const { data, error } = await query;
|
|
377
377
|
if (error)
|
|
378
378
|
return j(dbToolError(error));
|
|
379
|
-
return j({ issues: data ?? [], count: data?.length ?? 0 });
|
|
379
|
+
return j({ issues: data ?? [], count: data?.length ?? 0, truncated: (data?.length ?? 0) === limit });
|
|
380
380
|
});
|
|
381
381
|
server.registerTool('schedule_newsletter_issue', {
|
|
382
382
|
title: 'Schedule Newsletter Issue',
|
|
@@ -587,7 +587,7 @@ export function registerNewsletterTools(server) {
|
|
|
587
587
|
const { data, error } = await query;
|
|
588
588
|
if (error)
|
|
589
589
|
return j(dbToolError(error));
|
|
590
|
-
return j({ subscribers: data ?? [], count: data?.length ?? 0 });
|
|
590
|
+
return j({ subscribers: data ?? [], count: data?.length ?? 0, truncated: (data?.length ?? 0) === limit });
|
|
591
591
|
});
|
|
592
592
|
server.registerTool('get_newsletter_stats', {
|
|
593
593
|
title: 'Get Newsletter Stats',
|
|
@@ -107,9 +107,9 @@ export function registerShortLinkTools(server) {
|
|
|
107
107
|
return j(dbToolError(linkError));
|
|
108
108
|
if (!link)
|
|
109
109
|
return j({ error: 'Short link not found', code: 'not_found' });
|
|
110
|
-
const { data: clicks, error: clickError } = await db()
|
|
110
|
+
const { data: clicks, error: clickError, count: total } = await db()
|
|
111
111
|
.from('short_link_clicks')
|
|
112
|
-
.select('clicked_at, ip_hash, referer')
|
|
112
|
+
.select('clicked_at, ip_hash, referer', { count: 'exact' })
|
|
113
113
|
.eq('short_link_id', short_link_id)
|
|
114
114
|
.order('clicked_at', { ascending: false })
|
|
115
115
|
.limit(500);
|
|
@@ -118,7 +118,9 @@ export function registerShortLinkTools(server) {
|
|
|
118
118
|
const uniqueClicks = new Set((clicks ?? []).map((click) => click.ip_hash).filter(Boolean)).size;
|
|
119
119
|
return j({
|
|
120
120
|
short_link: link,
|
|
121
|
-
total_clicks: link.click_count ?? clicks?.length ?? 0,
|
|
121
|
+
total_clicks: link.click_count ?? total ?? clicks?.length ?? 0,
|
|
122
|
+
...(total != null ? { total } : {}),
|
|
123
|
+
truncated: (clicks?.length ?? 0) === 500,
|
|
122
124
|
unique_clicks: uniqueClicks,
|
|
123
125
|
recent_clicks: clicks ?? [],
|
|
124
126
|
});
|
package/build/tools/surveys.js
CHANGED
|
@@ -140,7 +140,7 @@ export function registerSurveyTools(server) {
|
|
|
140
140
|
status: z.enum(['draft', 'active', 'closed']).optional(),
|
|
141
141
|
limit: z.number().int().min(1).max(200).optional().default(50),
|
|
142
142
|
},
|
|
143
|
-
}, async ({ status, limit }) => {
|
|
143
|
+
}, async ({ status, limit = 50 }) => {
|
|
144
144
|
const projectId = getProjectId();
|
|
145
145
|
let query = db().from('surveys').select('id, title, status, short_link_id, created_at, updated_at').eq('project_id', projectId).order('created_at', { ascending: false }).limit(limit);
|
|
146
146
|
if (status)
|
|
@@ -148,7 +148,7 @@ export function registerSurveyTools(server) {
|
|
|
148
148
|
const { data, error } = await query;
|
|
149
149
|
if (error)
|
|
150
150
|
return j(dbToolError(error));
|
|
151
|
-
return j({ surveys: data ?? [], count: data?.length ?? 0 });
|
|
151
|
+
return j({ surveys: data ?? [], count: data?.length ?? 0, truncated: (data?.length ?? 0) === limit });
|
|
152
152
|
});
|
|
153
153
|
server.registerTool('get_survey', {
|
|
154
154
|
title: 'Get Survey',
|
package/build/tools/templates.js
CHANGED
|
@@ -93,7 +93,7 @@ export function registerTemplateTools(server) {
|
|
|
93
93
|
const { data, error } = await query;
|
|
94
94
|
if (error)
|
|
95
95
|
return j(dbToolError(error));
|
|
96
|
-
return j({ templates: data ?? [], count: data?.length ?? 0 });
|
|
96
|
+
return j({ templates: data ?? [], count: data?.length ?? 0, truncated: (data?.length ?? 0) === limit });
|
|
97
97
|
});
|
|
98
98
|
server.registerTool('translate_template', {
|
|
99
99
|
title: 'Translate Template',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.151",
|
|
4
4
|
"description": "AI-controlled email operations control plane for campaigns, contacts, segments, analytics, and multi-provider sending via MCP.",
|
|
5
5
|
"mcpName": "ai.sendinel/mcp-server",
|
|
6
6
|
"type": "module",
|