@sendinel/mcp-server 1.0.184 → 1.0.185

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.
@@ -0,0 +1,6 @@
1
+ /** Stored campaign rates are measurements only when an importer supplied their source. */
2
+ export declare function campaignMetricsView<T extends Record<string, unknown>>(campaign: T): T & {
3
+ open_rate: unknown;
4
+ click_rate: unknown;
5
+ unsubscribe_rate: unknown;
6
+ };
@@ -0,0 +1,9 @@
1
+ /** Stored campaign rates are measurements only when an importer supplied their source. */
2
+ export function campaignMetricsView(campaign) {
3
+ return {
4
+ ...campaign,
5
+ open_rate: campaign.metrics_source == null ? null : campaign.open_rate,
6
+ click_rate: campaign.metrics_source == null ? null : campaign.click_rate,
7
+ unsubscribe_rate: campaign.metrics_source == null ? null : campaign.unsubscribe_rate,
8
+ };
9
+ }
@@ -1,3 +1,4 @@
1
+ import { campaignMetricsView } from '../lib/campaign-metrics.js';
1
2
  import { withCreatedItems } from '../lib/mcp-created-items.js';
2
3
  import { validateStepMultiEventRules, stepMultiEventRulesView, validateStepAbsenceRule, stepAbsenceRuleView } from "@sendinel/domain";
3
4
  import { readCampaignMetricGroups, SENT_STATUSES } from "@sendinel/domain";
@@ -799,7 +800,7 @@ export function registerCampaignTools(server) {
799
800
  }, async ({ site_id, status, limit, period }) => {
800
801
  let q = db()
801
802
  .from('campaigns')
802
- .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, send_at')
803
+ .select('id, site_id, name, type, status, sends_per_hour, total_enrolled, total_sent, total_opened, total_clicked, open_rate, click_rate, unsubscribe_rate, metrics_source, 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, send_at')
803
804
  .eq('project_id', getProjectId())
804
805
  .order('created_at', { ascending: false })
805
806
  .limit(limit ?? 25);
@@ -818,7 +819,7 @@ export function registerCampaignTools(server) {
818
819
  const withPeople = await Promise.all(rows.map(async (row) => {
819
820
  const periods = await readCampaignPeriods(db(), getProjectId(), row);
820
821
  return {
821
- ...row,
822
+ ...campaignMetricsView(row),
822
823
  reentry_mode: row.period_key ? "per_event_period" : "once_ever",
823
824
  requires_approval_description: ACTIVATION_APPROVAL_DESCRIPTION,
824
825
  ...buildCampaignTriggerOutput(row, seenEvents),
@@ -938,7 +939,7 @@ export function registerCampaignTools(server) {
938
939
  if (error)
939
940
  return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
940
941
  const warnings = await campaignTimezoneWarnings(data);
941
- return { content: [{ type: 'text', text: JSON.stringify({ ...data, requires_approval_description: ACTIVATION_APPROVAL_DESCRIPTION, fires_on: buildCampaignFiresOn(data), trigger_diagnostics: buildCampaignTriggerDiagnostics(data), ...(warnings.length ? { warnings } : {}) }, null, 2) }] };
942
+ return { content: [{ type: 'text', text: JSON.stringify({ ...campaignMetricsView(data), requires_approval_description: ACTIVATION_APPROVAL_DESCRIPTION, fires_on: buildCampaignFiresOn(data), trigger_diagnostics: buildCampaignTriggerDiagnostics(data), ...(warnings.length ? { warnings } : {}) }, null, 2) }] };
942
943
  });
943
944
  const propertyConditions = z.array(z.union([
944
945
  z.object({ key: z.string(), op: z.enum(['eq', 'neq']), value: z.string() }).strict(),
@@ -1142,7 +1143,7 @@ export function registerCampaignTools(server) {
1142
1143
  status,
1143
1144
  }).catch(() => { });
1144
1145
  }
1145
- return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
1146
+ return { content: [{ type: 'text', text: JSON.stringify(campaignMetricsView(data), null, 2) }] };
1146
1147
  });
1147
1148
  server.registerTool('enroll_contact', {
1148
1149
  title: 'Enroll Contact',
@@ -1930,7 +1931,7 @@ export function registerCampaignTools(server) {
1930
1931
  details: { template_id, template_slug: template.slug, source: 'mcp' },
1931
1932
  }).catch(() => { });
1932
1933
  return j({
1933
- ...campaign,
1934
+ ...campaignMetricsView(campaign),
1934
1935
  steps_copied: steps.length,
1935
1936
  cloned_from_template_id: template_id,
1936
1937
  message: `Created draft campaign "${cloneName}" from template "${template.name}" (${steps.length} step(s) copied). It is a DRAFT — not active, and no contacts are enrolled.`,
@@ -3438,8 +3439,10 @@ Return ONLY valid JSON with this exact shape:
3438
3439
  }).catch(() => { }); // fire-and-forget
3439
3440
  }
3440
3441
  await logAudit({ action: 'campaign.created', resourceType: 'campaign', resourceId: campaign.id, details: { type: campaignType, ...targeting, activated: willActivate } });
3442
+ const { open_rate, click_rate, unsubscribe_rate } = campaignMetricsView(campaign);
3441
3443
  return j({
3442
3444
  campaign_id: campaign.id,
3445
+ open_rate, click_rate, unsubscribe_rate,
3443
3446
  name,
3444
3447
  type: campaignType,
3445
3448
  send_class: campaign.send_class,
@@ -3668,7 +3671,7 @@ Return ONLY valid JSON with this exact shape:
3668
3671
  }, async ({ approval_id, ...args }) => {
3669
3672
  if (approval_id) {
3670
3673
  const result = await resumeCampaignEdit('set_campaign_trigger', approval_id, args);
3671
- return j(result.error ? result : { ...result, ...await campaignTriggerOutput(result) });
3674
+ return j(result.error ? result : { ...campaignMetricsView(result), ...await campaignTriggerOutput(result) });
3672
3675
  }
3673
3676
  if (!args.campaign_id)
3674
3677
  return j({ error: 'campaign_id is required', code: 'validation_failed' });
@@ -3708,7 +3711,7 @@ Return ONLY valid JSON with this exact shape:
3708
3711
  if (reentry.period_key !== undefined)
3709
3712
  updates.period_key = reentry.period_key;
3710
3713
  const result = await finishCampaignEdit('set_campaign_trigger', args, existing, updates, params => requestApproval('set_campaign_trigger', params));
3711
- return j(result.error || result.requires_approval ? result : { ...result, ...await campaignTriggerOutput(result) });
3714
+ return j(result.error || result.requires_approval ? result : { ...campaignMetricsView(result), ...await campaignTriggerOutput(result) });
3712
3715
  });
3713
3716
  server.registerTool('update_campaign', {
3714
3717
  title: 'Update Campaign',
@@ -3744,7 +3747,7 @@ Return ONLY valid JSON with this exact shape:
3744
3747
  if (result.error)
3745
3748
  return j(result);
3746
3749
  const warnings = await campaignTimezoneWarnings(result);
3747
- return j({ ...result, ...await campaignTriggerOutput(result), ...(warnings.length ? { warnings } : {}) });
3750
+ return j({ ...campaignMetricsView(result), ...await campaignTriggerOutput(result), ...(warnings.length ? { warnings } : {}) });
3748
3751
  }
3749
3752
  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, reentry_mode, period_key } = input;
3750
3753
  if (!campaign_id)
@@ -3860,7 +3863,7 @@ Return ONLY valid JSON with this exact shape:
3860
3863
  const diagnostics = buildCampaignTriggerDiagnostics(data);
3861
3864
  const warning = diagnostics.find((d) => d.includes('never fire')) || (diagnostics.length ? diagnostics[0] : null);
3862
3865
  return j({
3863
- ...data,
3866
+ ...campaignMetricsView(data),
3864
3867
  fires_on: buildCampaignFiresOn(data),
3865
3868
  trigger_diagnostics: diagnostics,
3866
3869
  warnings: [...diagnostics, ...await campaignTimezoneWarnings(data)],
@@ -3948,7 +3951,7 @@ Return ONLY valid JSON with this exact shape:
3948
3951
  const failedCount = failedSendCount ?? 0;
3949
3952
  const unresolvedCount = dlqUnresolvedCount ?? 0;
3950
3953
  return j({
3951
- ...campaign,
3954
+ ...campaignMetricsView(campaign),
3952
3955
  reentry_mode: campaign.period_key ? "per_event_period" : "once_ever",
3953
3956
  requires_approval_description: ACTIVATION_APPROVAL_DESCRIPTION,
3954
3957
  ...await campaignTriggerOutput(campaign),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.184",
3
+ "version": "1.0.185",
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",