@sendinel/mcp-server 1.0.96 → 1.0.97
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 +21 -5
- package/package.json +1 -1
package/build/tools/campaigns.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CAMPAIGN_BRANCH_CONDITIONS, campaignBranchConditionValue } from "@sendinel/domain";
|
|
1
|
+
import { CAMPAIGN_BRANCH_CONDITIONS, campaignBranchConditionValue, isContactPropertyConditionList, STEP_PROPERTY_CONDITIONS_ERROR, validateStepPropertyRules } from "@sendinel/domain";
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import Anthropic from '@anthropic-ai/sdk';
|
|
4
4
|
import { assertSendAccepted, SendRefusedError, BLOCKING_CONTENT_ISSUE_KINDS, computeSendContentKey, findContentIssues, sendingHostOf, NAME_FALLBACKS, ONE_SEND_MATCH_STATUSES, ONE_SEND_SENT_STATUSES, ONE_SEND_WINDOW_DAYS, normalizeTriggerConfig, renderTemplateString as renderTemplateVars, resolveBrandTokens, brandWarnings, readAllRows, } from '@sendinel/domain';
|
|
@@ -764,6 +764,10 @@ export function registerCampaignTools(server) {
|
|
|
764
764
|
return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
|
|
765
765
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
766
766
|
});
|
|
767
|
+
const propertyConditions = z.array(z.union([
|
|
768
|
+
z.object({ key: z.string(), op: z.enum(['eq', 'neq']), value: z.string() }).strict(),
|
|
769
|
+
z.object({ key: z.string(), op: z.literal('exists') }).strict(),
|
|
770
|
+
])).min(1).max(10).refine(isContactPropertyConditionList, STEP_PROPERTY_CONDITIONS_ERROR).nullable().optional();
|
|
767
771
|
server.registerTool('add_campaign_step', {
|
|
768
772
|
title: 'Add Campaign Step',
|
|
769
773
|
annotations: {
|
|
@@ -785,10 +789,15 @@ export function registerCampaignTools(server) {
|
|
|
785
789
|
variant_label: z.string().optional().default('A').describe('Variant label for A/B testing (default: A). Use B, C, etc. for additional variants.'),
|
|
786
790
|
survey_id: z.string().uuid().optional(),
|
|
787
791
|
brand_asset_ids: z.array(z.string().uuid()).optional().describe('Stable brand_media_assets ids explicitly referenced by this step.'),
|
|
792
|
+
skip_if_property: propertyConditions.describe('Skip when ALL 1–10 conditions match current custom_properties. Missing/null keys never match. null clears.'),
|
|
793
|
+
send_only_if_property: propertyConditions.describe('Send only when ALL 1–10 conditions match current custom_properties. Unknown/non-match skips. null clears.'),
|
|
788
794
|
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.'),
|
|
789
795
|
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.'),
|
|
790
796
|
},
|
|
791
|
-
}, 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 }) => {
|
|
797
|
+
}, 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 }) => {
|
|
798
|
+
const properties = validateStepPropertyRules({ skip_if_property, send_only_if_property });
|
|
799
|
+
if (!properties.ok)
|
|
800
|
+
return j({ error: properties.error, code: "INVALID_INPUT" });
|
|
792
801
|
const renderVars = {};
|
|
793
802
|
if (survey_id) {
|
|
794
803
|
const surveyLink = await resolveSurveyShortLink(db(), getProjectId(), survey_id);
|
|
@@ -803,7 +812,7 @@ export function registerCampaignTools(server) {
|
|
|
803
812
|
const renderedText = body_text !== undefined && survey_id ? renderTemplateVars(body_text, renderVars) : body_text;
|
|
804
813
|
const { data, error } = await db()
|
|
805
814
|
.from('campaign_steps')
|
|
806
|
-
.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 })
|
|
815
|
+
.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 })
|
|
807
816
|
.select()
|
|
808
817
|
.single();
|
|
809
818
|
if (error)
|
|
@@ -818,6 +827,7 @@ export function registerCampaignTools(server) {
|
|
|
818
827
|
}).catch((err) => {
|
|
819
828
|
console.warn('Failed to record brand asset usage for MCP campaign step:', err);
|
|
820
829
|
});
|
|
830
|
+
await logAudit({ action: 'campaign.step_created', resourceType: 'campaign_step', resourceId: data.id, details: { property_rules: properties.rules } });
|
|
821
831
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
822
832
|
});
|
|
823
833
|
server.registerTool('update_campaign_status', {
|
|
@@ -3101,7 +3111,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3101
3111
|
return j({ error: campaignErr?.message ?? 'Campaign not found', code: 'NOT_FOUND' });
|
|
3102
3112
|
const { data, error } = await db()
|
|
3103
3113
|
.from('campaign_steps')
|
|
3104
|
-
.select('id, step_order, subject, body_html, body_text, blocks, delay_days, delay_hours, skip_if_event, send_only_if_event, created_at, updated_at')
|
|
3114
|
+
.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')
|
|
3105
3115
|
.eq('campaign_id', campaign_id)
|
|
3106
3116
|
.order('step_order', { ascending: true });
|
|
3107
3117
|
if (error)
|
|
@@ -3127,6 +3137,8 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3127
3137
|
delay_hours: z.number().int().min(0).optional(),
|
|
3128
3138
|
step_order: z.number().int().min(0).optional(),
|
|
3129
3139
|
survey_id: z.string().uuid().optional(),
|
|
3140
|
+
skip_if_property: propertyConditions.describe('Skip when ALL 1–10 conditions match current custom_properties. Missing/null keys never match. null clears.'),
|
|
3141
|
+
send_only_if_property: propertyConditions.describe('Send only when ALL 1–10 conditions match current custom_properties. Unknown/non-match skips. null clears.'),
|
|
3130
3142
|
skip_if_event: z.string().trim().min(1).nullable().optional().describe('Event name that skips this step at send time; null clears the rule.'),
|
|
3131
3143
|
send_only_if_event: z.string().trim().min(1).nullable().optional().describe('Event required for this step to send; null clears the rule.'),
|
|
3132
3144
|
link_actions: z.array(z.discriminatedUnion('action', [
|
|
@@ -3143,6 +3155,9 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3143
3155
|
}, async ({ step_id, ...updates }) => {
|
|
3144
3156
|
const projectId = getProjectId();
|
|
3145
3157
|
const { survey_id, ...stepUpdates } = updates;
|
|
3158
|
+
const properties = validateStepPropertyRules(stepUpdates);
|
|
3159
|
+
if (!properties.ok)
|
|
3160
|
+
return j({ error: properties.error, code: "INVALID_INPUT" });
|
|
3146
3161
|
const { data: step, error: stepErr } = await db()
|
|
3147
3162
|
.from('campaign_steps')
|
|
3148
3163
|
.select('id, campaign_id')
|
|
@@ -3179,10 +3194,11 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3179
3194
|
.from('campaign_steps')
|
|
3180
3195
|
.update({ ...renderedUpdates, updated_at: new Date().toISOString() })
|
|
3181
3196
|
.eq('id', step_id)
|
|
3182
|
-
.select('id, step_order, subject, delay_days, delay_hours, skip_if_event, send_only_if_event, updated_at')
|
|
3197
|
+
.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')
|
|
3183
3198
|
.single();
|
|
3184
3199
|
if (error)
|
|
3185
3200
|
return j({ error: error.message });
|
|
3201
|
+
await logAudit({ action: 'campaign.step_updated', resourceType: 'campaign_step', resourceId: data.id, details: { property_rules: properties.rules } });
|
|
3186
3202
|
return j({ step: data, ok: true });
|
|
3187
3203
|
});
|
|
3188
3204
|
server.registerTool('delete_campaign_step', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.97",
|
|
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",
|