@sendinel/mcp-server 1.0.107 → 1.0.108
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 +27 -6
- package/package.json +1 -1
package/build/tools/campaigns.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readCampaignPeriods, campaignPeriodFields } from "@sendinel/domain";
|
|
|
2
2
|
import { CAMPAIGN_BRANCH_CONDITIONS, campaignBranchConditionValue, isContactPropertyConditionList, STEP_PROPERTY_CONDITIONS_ERROR, validateStepPropertyRules } from "@sendinel/domain";
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import Anthropic from '@anthropic-ai/sdk';
|
|
5
|
-
import { assertSendAccepted, providerSendError, sendError, 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, isSegmentRulesError, scopeSegmentRulesToSite, SegmentRulesError, validateSegmentRules, } from '@sendinel/domain';
|
|
5
|
+
import { assertSendAccepted, providerSendError, sendError, 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, isSegmentRulesError, scopeSegmentRulesToSite, SegmentRulesError, validateSegmentRules, validateUnsubFooterTemplate, } from '@sendinel/domain';
|
|
6
6
|
// Subpath import, not the barrel above -- reply-actions.ts uses node:crypto,
|
|
7
7
|
// and MCP-server-side that's fine (this never runs in an Edge Function), but
|
|
8
8
|
// keeping it off the shared barrel avoids re-creating the Edge-Function
|
|
@@ -652,7 +652,7 @@ export function registerCampaignTools(server) {
|
|
|
652
652
|
}, async ({ site_id, status, limit, period }) => {
|
|
653
653
|
let q = db()
|
|
654
654
|
.from('campaigns')
|
|
655
|
-
.select('id, site_id, name, type, status, sends_per_hour, total_enrolled, total_sent, total_opened, total_clicked, created_at, period_key, segment_id, trigger_type, trigger_event_name, trigger_tag, trigger_config')
|
|
655
|
+
.select('id, site_id, name, type, status, sends_per_hour, total_enrolled, total_sent, total_opened, total_clicked, created_at, period_key, segment_id, trigger_type, trigger_event_name, trigger_tag, trigger_config, send_class, unsub_footer_template')
|
|
656
656
|
.eq('project_id', getProjectId())
|
|
657
657
|
.order('created_at', { ascending: false })
|
|
658
658
|
.limit(limit ?? 25);
|
|
@@ -2966,6 +2966,8 @@ Return ONLY valid JSON with this exact shape:
|
|
|
2966
2966
|
site_id: z.string().uuid().describe('Site to send from'),
|
|
2967
2967
|
name: z.string().min(1).max(200).describe('Campaign name'),
|
|
2968
2968
|
type: z.enum(['sequence', 'broadcast', 'triggered']).describe('Campaign type'),
|
|
2969
|
+
send_class: z.enum(['broadcast', 'product']).optional().describe("Consent class: broadcast (marketing, default) or product (account, onboarding, trial and billing mail)."),
|
|
2970
|
+
unsub_footer_template: z.string().nullable().optional().describe('Campaign unsubscribe footer override. Must contain <unsubscribe>link text</unsubscribe>. Null uses the site footer.'),
|
|
2969
2971
|
trigger_event_name: z.string().nullable().optional(),
|
|
2970
2972
|
trigger_tag: z.string().nullable().optional(),
|
|
2971
2973
|
trigger_type: z.enum(['manual', 'date_field', 'contact_event']).nullable().optional(),
|
|
@@ -2984,7 +2986,14 @@ Return ONLY valid JSON with this exact shape:
|
|
|
2984
2986
|
confirmed: z.boolean().optional().default(false).describe('Two-call confirmation. First call previews, second call with confirmed=true executes.'),
|
|
2985
2987
|
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.'),
|
|
2986
2988
|
},
|
|
2987
|
-
}, async ({ site_id, name, type, steps, segment_id, sends_per_hour, activate, confirmed, approval_id, trigger_event_name, trigger_tag, trigger_type, trigger_config }) => {
|
|
2989
|
+
}, 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 }) => {
|
|
2990
|
+
const footerTemplate = unsub_footer_template?.trim() ?? null;
|
|
2991
|
+
if (footerTemplate !== null) {
|
|
2992
|
+
const footerError = validateUnsubFooterTemplate(footerTemplate);
|
|
2993
|
+
if (footerError)
|
|
2994
|
+
return j({ error: footerError, code: 'INVALID_FIELD' });
|
|
2995
|
+
}
|
|
2996
|
+
const sendClass = send_class ?? 'broadcast';
|
|
2988
2997
|
const targeting = {};
|
|
2989
2998
|
const triggerInput = { trigger_event_name, trigger_tag, trigger_type, trigger_config };
|
|
2990
2999
|
const targetingError = await applyCampaignTargetingFields(targeting, triggerInput, getProjectId());
|
|
@@ -2996,7 +3005,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
2996
3005
|
const canActivateOnTrigger = campaignType === 'triggered' && (hasEventOrTag || targeting.trigger_type === 'date_field' || targeting.trigger_type === 'contact_event');
|
|
2997
3006
|
// Human-in-the-loop gate for activation
|
|
2998
3007
|
if (activate && confirmed) {
|
|
2999
|
-
const approvalParams = { site_id, name, type, steps, segment_id, sends_per_hour, activate, ...triggerInput };
|
|
3008
|
+
const approvalParams = { site_id, name, type, steps, segment_id, sends_per_hour, activate, send_class, unsub_footer_template, ...triggerInput };
|
|
3000
3009
|
const channelContext = currentChannelContext();
|
|
3001
3010
|
const decision = assertChannelCanExecute('create_campaign_with_content', { activate }, channelContext);
|
|
3002
3011
|
if (!decision.allowed)
|
|
@@ -3042,7 +3051,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3042
3051
|
if (!confirmed) {
|
|
3043
3052
|
return j({
|
|
3044
3053
|
confirmation_required: true,
|
|
3045
|
-
campaign: { name, type: campaignType, site: site.name, ...targeting },
|
|
3054
|
+
campaign: { name, type: campaignType, site: site.name, send_class: sendClass, unsub_footer_template: footerTemplate, ...targeting },
|
|
3046
3055
|
steps: steps.map((s, i) => ({
|
|
3047
3056
|
step: i + 1,
|
|
3048
3057
|
subject: s.subject,
|
|
@@ -3066,6 +3075,8 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3066
3075
|
site_id,
|
|
3067
3076
|
name,
|
|
3068
3077
|
type: campaignType,
|
|
3078
|
+
send_class: sendClass,
|
|
3079
|
+
unsub_footer_template: footerTemplate,
|
|
3069
3080
|
...targeting,
|
|
3070
3081
|
status: 'draft',
|
|
3071
3082
|
segment_id: segment_id ?? null,
|
|
@@ -3150,6 +3161,8 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3150
3161
|
campaign_id: campaign.id,
|
|
3151
3162
|
name,
|
|
3152
3163
|
type: campaignType,
|
|
3164
|
+
send_class: campaign.send_class,
|
|
3165
|
+
unsub_footer_template: campaign.unsub_footer_template,
|
|
3153
3166
|
...targeting,
|
|
3154
3167
|
fires_on: buildCampaignFiresOn({ ...campaign, status: willActivate ? 'active' : 'draft' }),
|
|
3155
3168
|
trigger_diagnostics: buildCampaignTriggerDiagnostics({ ...campaign, status: willActivate ? 'active' : 'draft' }),
|
|
@@ -3415,6 +3428,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3415
3428
|
type: z.enum(['sequence', 'triggered']).optional().describe("Switch a DRAFT campaign between 'sequence' and 'triggered'. Event and tag triggers only start campaigns of type 'triggered'."),
|
|
3416
3429
|
sends_per_hour: z.number().int().min(1).nullable().optional().describe('Optional hourly send cap for wave sending. Set null to clear.'),
|
|
3417
3430
|
send_in_contact_timezone: z.boolean().optional(),
|
|
3431
|
+
unsub_footer_template: z.string().nullable().optional().describe('Campaign unsubscribe footer override. Must contain <unsubscribe>link text</unsubscribe>. Set null to use the site footer.'),
|
|
3418
3432
|
send_class: z.enum(['broadcast', 'product']).optional().describe("Who may receive it when the project's consent gate is on: 'broadcast' (default) = only contacts who opted in to marketing; 'product' = also the customer's signed-up users (onboarding, trial, account mail)."),
|
|
3419
3433
|
},
|
|
3420
3434
|
}, async ({ approval_id, ...input }) => {
|
|
@@ -3422,9 +3436,14 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3422
3436
|
const result = await resumeCampaignEdit('update_campaign', approval_id, input);
|
|
3423
3437
|
return j(result.error ? result : { ...result, fires_on: buildCampaignFiresOn(result), trigger_diagnostics: buildCampaignTriggerDiagnostics(result) });
|
|
3424
3438
|
}
|
|
3425
|
-
const { campaign_id, name, site_id, segment_id, send_at, topic, trigger_event_name, trigger_tag, trigger_type, trigger_config, type, sends_per_hour, send_in_contact_timezone, send_class } = input;
|
|
3439
|
+
const { campaign_id, name, site_id, segment_id, send_at, topic, trigger_event_name, trigger_tag, trigger_type, trigger_config, type, sends_per_hour, send_in_contact_timezone, send_class, unsub_footer_template } = input;
|
|
3426
3440
|
if (!campaign_id)
|
|
3427
3441
|
return j({ error: 'campaign_id is required', code: 'INVALID_FIELD' });
|
|
3442
|
+
if (unsub_footer_template !== undefined && unsub_footer_template !== null) {
|
|
3443
|
+
const footerError = validateUnsubFooterTemplate(unsub_footer_template);
|
|
3444
|
+
if (footerError)
|
|
3445
|
+
return j({ error: footerError, code: 'INVALID_FIELD' });
|
|
3446
|
+
}
|
|
3428
3447
|
const projectId = getProjectId();
|
|
3429
3448
|
let campaignType;
|
|
3430
3449
|
const loadExisting = () => db()
|
|
@@ -3491,6 +3510,8 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3491
3510
|
updates.send_in_contact_timezone = send_in_contact_timezone;
|
|
3492
3511
|
if (send_class !== undefined)
|
|
3493
3512
|
updates.send_class = send_class;
|
|
3513
|
+
if (unsub_footer_template !== undefined)
|
|
3514
|
+
updates.unsub_footer_template = unsub_footer_template?.trim() ?? null;
|
|
3494
3515
|
const targetingError = await applyCampaignTargetingFields(updates, { trigger_event_name, trigger_tag, trigger_type, trigger_config, segment_id }, projectId, existingCampaignForTargeting);
|
|
3495
3516
|
if (targetingError)
|
|
3496
3517
|
return j({ error: targetingError, code: targetingError === 'Segment not found' ? 'NOT_FOUND' : 'INVALID_FIELD' });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.108",
|
|
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",
|