@sendinel/mcp-server 1.0.113 → 1.0.115

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.
@@ -31,6 +31,7 @@ import { callSendinelV1 } from '../lib/v1-client.js';
31
31
  import { countUniqueEngagementByCampaign } from '../lib/campaign-engagement.js';
32
32
  const txt = (text) => ({ content: [{ type: 'text', text }] });
33
33
  const j = (data) => txt(JSON.stringify(data, null, 2));
34
+ const ACTIVATION_APPROVAL_DESCRIPTION = 'Activation-only: requires_approval gates campaign activation. It does not stop sends of an already-active campaign or sends triggered by other paths.';
34
35
  const SENT_EMAIL_STATUSES = ['sent', 'delivered', 'opened', 'clicked', 'bounced', 'complained', 'unsubscribed'];
35
36
  function stripHtml(value) {
36
37
  return value
@@ -557,13 +558,13 @@ export function registerCampaignTools(server) {
557
558
  idempotentHint: true,
558
559
  openWorldHint: true,
559
560
  },
560
- description: 'Run the campaign portfolio review and optionally raise agent proposals for start/end/modify recommendations.',
561
+ description: 'Run the campaign portfolio review. Defaults to a dry run (no writes); pass dry_run=false to raise agent proposals for start/end/modify recommendations and emit recommendation signals.',
561
562
  inputSchema: {
562
- dry_run: z.boolean().optional().default(false),
563
+ dry_run: z.boolean().optional().default(true).describe('Preview only by default: computes findings and writes nothing. Pass false to raise agent proposals and emit recommendation signals.'),
563
564
  dormant_draft_days: z.number().int().min(1).max(365).optional(),
564
565
  abandoned_days: z.number().int().min(1).max(365).optional(),
565
566
  },
566
- }, async ({ dry_run = false, dormant_draft_days, abandoned_days }) => {
567
+ }, async ({ dry_run = true, dormant_draft_days, abandoned_days }) => {
567
568
  const result = await callSendinelV1('/api/v1/campaigns/review', {
568
569
  body: {
569
570
  dry_run,
@@ -642,7 +643,7 @@ export function registerCampaignTools(server) {
642
643
  idempotentHint: true,
643
644
  openWorldHint: false,
644
645
  },
645
- description: 'List campaigns with saved trigger fields, fires_on summaries and trigger_diagnostics; optionally filter by site or status.',
646
+ description: 'List campaigns with saved trigger fields, fires_on summaries and trigger_diagnostics; optionally filter by site or status. ' + ACTIVATION_APPROVAL_DESCRIPTION,
646
647
  inputSchema: {
647
648
  site_id: z.string().uuid().optional(),
648
649
  period: z.string().min(1).optional().describe("Return one enrollment period in by_period; totals remain all-time."),
@@ -652,7 +653,7 @@ export function registerCampaignTools(server) {
652
653
  }, async ({ site_id, status, limit, period }) => {
653
654
  let q = db()
654
655
  .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, send_class, unsub_footer_template')
656
+ .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, requires_approval, approved_by, approved_at')
656
657
  .eq('project_id', getProjectId())
657
658
  .order('created_at', { ascending: false })
658
659
  .limit(limit ?? 25);
@@ -672,6 +673,7 @@ export function registerCampaignTools(server) {
672
673
  const { period_key: _periodKey, ...legacy } = row;
673
674
  return {
674
675
  ...legacy,
676
+ requires_approval_description: ACTIVATION_APPROVAL_DESCRIPTION,
675
677
  fires_on: buildCampaignFiresOn(row),
676
678
  trigger_diagnostics: buildCampaignTriggerDiagnostics(row),
677
679
  ...campaignPeriodFields(periods, period),
@@ -735,7 +737,7 @@ export function registerCampaignTools(server) {
735
737
  type: z.enum(['sequence', 'broadcast', 'triggered']),
736
738
  description: z.string().optional(),
737
739
  sends_per_hour: z.number().int().min(1).nullable().optional().describe('Optional hourly send cap for wave sending. Null or omitted sends without a campaign-specific cap.'),
738
- requires_approval: z.boolean().optional().default(true),
740
+ requires_approval: z.boolean().optional().default(true).describe(ACTIVATION_APPROVAL_DESCRIPTION),
739
741
  trigger_event_name: z.string().optional().describe('For type=triggered: event name that fires this campaign'),
740
742
  trigger_tag: z.string().optional().describe('For type=triggered: contact tag that fires this campaign when applied'),
741
743
  trigger_type: z.enum(['date_field']).optional().describe('Set for elapsed-time triggers keyed on a contact custom_properties date field'),
@@ -773,7 +775,7 @@ export function registerCampaignTools(server) {
773
775
  .single();
774
776
  if (error)
775
777
  return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
776
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
778
+ return { content: [{ type: 'text', text: JSON.stringify({ ...data, requires_approval_description: ACTIVATION_APPROVAL_DESCRIPTION }, null, 2) }] };
777
779
  });
778
780
  const propertyConditions = z.array(z.union([
779
781
  z.object({ key: z.string(), op: z.enum(['eq', 'neq']), value: z.string() }).strict(),
@@ -849,7 +851,7 @@ export function registerCampaignTools(server) {
849
851
  idempotentHint: true,
850
852
  openWorldHint: true,
851
853
  },
852
- description: 'Activate, pause, or archive a campaign. Activating requires human approval via the dashboard — call without approval_id to request one, then call again with the approved token.',
854
+ description: 'Activate, pause, or archive a campaign. Activating requires human approval via the dashboard — call without approval_id to request one, then call again with the approved token. ' + ACTIVATION_APPROVAL_DESCRIPTION,
853
855
  inputSchema: {
854
856
  campaign_id: z.string().uuid(),
855
857
  status: z.enum(['draft', 'active', 'paused', 'completed', 'archived']),
@@ -3558,7 +3560,7 @@ Return ONLY valid JSON with this exact shape:
3558
3560
  idempotentHint: true,
3559
3561
  openWorldHint: false,
3560
3562
  },
3561
- description: 'Get full campaign details including all steps, enrollment counts and saved trigger_type, trigger_event_name, trigger_tag, trigger_config (including where filter), fires_on and trigger_diagnostics. Read what event a dunning campaign fires on; use set_campaign_trigger to change it.',
3563
+ description: 'Get full campaign details including all steps, enrollment counts and saved trigger_type, trigger_event_name, trigger_tag, trigger_config (including where filter), fires_on and trigger_diagnostics. Read what event a dunning campaign fires on; use set_campaign_trigger to change it. ' + ACTIVATION_APPROVAL_DESCRIPTION,
3562
3564
  inputSchema: { campaign_id: z.string().uuid(), period: z.string().min(1).optional().describe("Return results for one enrollment period; totals remain all-time.") },
3563
3565
  }, async ({ campaign_id, period }) => {
3564
3566
  const projectId = getProjectId();
@@ -3629,6 +3631,7 @@ Return ONLY valid JSON with this exact shape:
3629
3631
  const unresolvedCount = dlqUnresolvedCount ?? 0;
3630
3632
  return j({
3631
3633
  ...campaign,
3634
+ requires_approval_description: ACTIVATION_APPROVAL_DESCRIPTION,
3632
3635
  fires_on: buildCampaignFiresOn(campaign),
3633
3636
  trigger_diagnostics: buildCampaignTriggerDiagnostics(campaign),
3634
3637
  total_sent: periods?.totals?.sent ?? sentCount ?? 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.113",
3
+ "version": "1.0.115",
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",