@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.
@@ -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.96",
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",