@sendinel/mcp-server 1.0.141 → 1.0.142
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.
|
@@ -13,7 +13,7 @@ export function registerAbTestingTools(server) {
|
|
|
13
13
|
idempotentHint: false,
|
|
14
14
|
openWorldHint: false,
|
|
15
15
|
},
|
|
16
|
-
description: 'Create an A/B test for a campaign step. Requires at least 2 variant steps (add via add_campaign_step with variant_label). Returns the test config for review.',
|
|
16
|
+
description: 'Create an A/B test for a campaign step. Requires a non-blank subject and body on every variant, and at least 2 variant steps (add via add_campaign_step with variant_label). Returns the test config for review.',
|
|
17
17
|
inputSchema: {
|
|
18
18
|
campaign_id: z.string().uuid(),
|
|
19
19
|
step_order: z.number().int().min(0).optional().default(0).describe('Which step to test (default: 0, the first email)'),
|
|
@@ -38,13 +38,20 @@ export function registerAbTestingTools(server) {
|
|
|
38
38
|
// Check variant steps exist
|
|
39
39
|
const { data: steps } = await db()
|
|
40
40
|
.from('campaign_steps')
|
|
41
|
-
.select('id, variant_label, subject')
|
|
41
|
+
.select('id, variant_label, subject, body_html, body_text')
|
|
42
42
|
.eq('campaign_id', campaign_id)
|
|
43
43
|
.eq('step_order', step_order ?? 0);
|
|
44
44
|
const variantLabels = (steps ?? []).map((s) => s.variant_label);
|
|
45
45
|
if (variantLabels.length < 2) {
|
|
46
46
|
return txt(`Error: Need at least 2 variant steps for step_order ${step_order ?? 0}. Found: [${variantLabels.join(', ')}]. Add more variants via add_campaign_step with variant_label parameter.`);
|
|
47
47
|
}
|
|
48
|
+
const emptyVariants = (steps ?? []).filter((step) => !step.subject?.trim() || !(step.body_html?.trim() || step.body_text?.trim()));
|
|
49
|
+
if (emptyVariants.length) {
|
|
50
|
+
return j({
|
|
51
|
+
error: `Variant(s) [${emptyVariants.map((step) => step.variant_label).join(', ')}] for step_order ${step_order ?? 0} need a non-blank subject and body. Add content with add_campaign_step before creating the test.`,
|
|
52
|
+
code: 'validation_failed',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
48
55
|
// Check no existing test for this step
|
|
49
56
|
const { data: existing } = await db()
|
|
50
57
|
.from('ab_tests')
|
package/build/tools/campaigns.js
CHANGED
|
@@ -137,6 +137,9 @@ export function buildCampaignTriggerDiagnostics(campaign, seenEvents) {
|
|
|
137
137
|
if ((campaign.trigger_type === 'date_field' || campaign.trigger_type === 'contact_event') && (events.length || tags.length) && campaign.type === 'triggered') {
|
|
138
138
|
diagnostics.push(`trigger_type is "${String(campaign.trigger_type)}" but event/tag triggers are also set; event intake will start it immediately, outside the ${String(campaign.trigger_type)} schedule`);
|
|
139
139
|
}
|
|
140
|
+
if (campaign.type === 'triggered' && campaignTriggerKind(campaign) === 'manual' && !campaign.segment_id) {
|
|
141
|
+
diagnostics.push('campaign type is "triggered" but no trigger is set (it will never fire)');
|
|
142
|
+
}
|
|
140
143
|
if (campaign.status !== 'active') {
|
|
141
144
|
diagnostics.push(`it is ${String(campaign.status)}, so nothing starts until it is active`);
|
|
142
145
|
}
|
|
@@ -855,7 +858,7 @@ export function registerCampaignTools(server) {
|
|
|
855
858
|
.single();
|
|
856
859
|
if (error)
|
|
857
860
|
return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
|
|
858
|
-
return { content: [{ type: 'text', text: JSON.stringify({ ...data, requires_approval_description: ACTIVATION_APPROVAL_DESCRIPTION }, null, 2) }] };
|
|
861
|
+
return { content: [{ type: 'text', text: JSON.stringify({ ...data, requires_approval_description: ACTIVATION_APPROVAL_DESCRIPTION, fires_on: buildCampaignFiresOn(data), trigger_diagnostics: buildCampaignTriggerDiagnostics(data) }, null, 2) }] };
|
|
859
862
|
});
|
|
860
863
|
const propertyConditions = z.array(z.union([
|
|
861
864
|
z.object({ key: z.string(), op: z.enum(['eq', 'neq']), value: z.string() }).strict(),
|
|
@@ -869,7 +872,7 @@ export function registerCampaignTools(server) {
|
|
|
869
872
|
idempotentHint: false,
|
|
870
873
|
openWorldHint: false,
|
|
871
874
|
},
|
|
872
|
-
description: 'Add an email step to a campaign. For A/B testing, add multiple steps with the same step_order but different variant_label values.',
|
|
875
|
+
description: 'Add an email step to a campaign. Requires a non-blank subject and body_html, body_text, or blocks that render to content. For A/B testing, add multiple steps with the same step_order but different variant_label values.',
|
|
873
876
|
inputSchema: {
|
|
874
877
|
campaign_id: z.string().uuid(),
|
|
875
878
|
step_order: z.number().int().min(0).describe('Position in sequence (0 = first)'),
|
|
@@ -892,6 +895,16 @@ export function registerCampaignTools(server) {
|
|
|
892
895
|
const properties = validateStepPropertyRules({ skip_if_property, send_only_if_property });
|
|
893
896
|
if (!properties.ok)
|
|
894
897
|
return j({ error: properties.error, code: "INVALID_INPUT" });
|
|
898
|
+
const normalizedBlocks = blocks ? normalizeComposerBlocks(blocks) : undefined;
|
|
899
|
+
const htmlFromBlocks = normalizedBlocks?.length ? blocksToHtml(normalizedBlocks) : undefined;
|
|
900
|
+
if (!subject.trim())
|
|
901
|
+
return j({ error: 'A step needs a non-blank subject.', code: 'validation_failed' });
|
|
902
|
+
const hasBody = body_html?.trim() || body_text?.trim()
|
|
903
|
+
|| htmlFromBlocks?.replace(/<[^>]*>/g, '').trim()
|
|
904
|
+
|| normalizedBlocks?.some((block) => block.type === 'image' && block.url);
|
|
905
|
+
if (!hasBody) {
|
|
906
|
+
return j({ error: 'A step needs body_html or body_text (or blocks that render to content).', code: 'validation_failed' });
|
|
907
|
+
}
|
|
895
908
|
const { data: campaign, error: campaignError } = await db()
|
|
896
909
|
.schema('email')
|
|
897
910
|
.from('campaigns')
|
|
@@ -911,8 +924,6 @@ export function registerCampaignTools(server) {
|
|
|
911
924
|
renderVars.survey_link = surveyLink;
|
|
912
925
|
}
|
|
913
926
|
const renderedSubject = survey_id ? renderTemplateVars(subject, renderVars) : subject;
|
|
914
|
-
const normalizedBlocks = blocks ? normalizeComposerBlocks(blocks) : undefined;
|
|
915
|
-
const htmlFromBlocks = normalizedBlocks?.length ? blocksToHtml(normalizedBlocks) : undefined;
|
|
916
927
|
const renderedHtml = body_html !== undefined && survey_id ? renderTemplateVars(body_html, renderVars) : body_html ?? htmlFromBlocks;
|
|
917
928
|
const renderedText = body_text !== undefined && survey_id ? renderTemplateVars(body_text, renderVars) : body_text;
|
|
918
929
|
const { data, error } = await db()
|
|
@@ -975,12 +986,15 @@ export function registerCampaignTools(server) {
|
|
|
975
986
|
// Placeholder text and links to the sending domain never go out (#3853).
|
|
976
987
|
const { data: campRow, error: campErr } = await db()
|
|
977
988
|
.from('campaigns')
|
|
978
|
-
.select('id, name, site_id, trigger_event_name')
|
|
989
|
+
.select('id, name, site_id, type, trigger_type, trigger_event_name, trigger_tag, trigger_config, segment_id')
|
|
979
990
|
.eq('id', campaign_id)
|
|
980
991
|
.eq('project_id', getProjectId())
|
|
981
992
|
.maybeSingle();
|
|
982
993
|
if (campErr)
|
|
983
994
|
return j({ error: `Could not check the campaign: ${campErr.message}`, code: 'CAMPAIGN_LOOKUP_FAILED' });
|
|
995
|
+
if (campRow?.type === 'triggered' && campaignTriggerKind(campRow) === 'manual' && !campRow.segment_id) {
|
|
996
|
+
return j({ error: 'This triggered campaign has no trigger. Set an event, tag, date, contact event, or segment before activating it.', code: 'validation_failed' });
|
|
997
|
+
}
|
|
984
998
|
let contentWarnings = [];
|
|
985
999
|
if (campRow) {
|
|
986
1000
|
const { data: contentSteps, error: contentErr } = await db()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.142",
|
|
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",
|