@sendinel/mcp-server 1.0.147 → 1.0.149

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.
@@ -92,6 +92,8 @@ export const API_USAGE_ERROR_CODE_MAP = {
92
92
  'unprocessable_entity': 'validation_failed',
93
93
  'validation': 'validation_failed',
94
94
  'validation_failed': 'validation_failed',
95
+ 'approval_not_cancellable': 'conflict',
96
+ 'approval_not_pending': 'conflict',
95
97
  '23505': 'conflict',
96
98
  'conflict': 'conflict',
97
99
  'content_changed_since_approval': 'conflict',
@@ -131,7 +133,10 @@ export const API_USAGE_ERROR_CODE_MAP = {
131
133
  'activation_failed': 'internal_error',
132
134
  'ai_not_configured': 'internal_error',
133
135
  'ai_unavailable': 'internal_error',
136
+ 'approval_audit_failed': 'internal_error',
137
+ 'approval_failed': 'internal_error',
134
138
  'approval_lookup_failed': 'internal_error',
139
+ 'approval_update_failed': 'internal_error',
135
140
  'audience_lookup_failed': 'internal_error',
136
141
  'campaign_create_failed': 'internal_error',
137
142
  'campaign_lookup_failed': 'internal_error',
@@ -1,3 +1,4 @@
1
+ import { isSentRow } from "@sendinel/domain";
1
2
  import { readPeriodCampaigns, campaignPeriodFields, filterPlanContacts } from "@sendinel/domain";
2
3
  import { isTrueConversionEvent, TRUE_CONVERSION_DEFINITION } from "@sendinel/domain";
3
4
  import { percentRate, groupMetricRows, readRevenue, isComplainedRow, isOpenedRow, countPeople, isClickedRow, summarizeEmailRows, countEmailRows, isDeliveredRow, isBouncedRow, fetchAllRows, readAllRows } from "@sendinel/domain";
@@ -42,7 +43,7 @@ export function computeSenderHealthScore(metrics, trackingEnabled) {
42
43
  function sendVolumeByDay(rows) {
43
44
  const byDay = new Map();
44
45
  for (const row of rows) {
45
- if (!row.sent_at)
46
+ if (!row.sent_at || !isSentRow(row))
46
47
  continue;
47
48
  const date = new Date(row.sent_at).toISOString().slice(0, 10);
48
49
  byDay.set(date, (byDay.get(date) ?? 0) + 1);
@@ -133,12 +134,12 @@ function distinctEventCount(events, eventType, messageIds) {
133
134
  .map(event => event.provider_message_id)).size;
134
135
  }
135
136
  function buildTrueConversionAnalytics(params) {
136
- const sent = params.logs.length;
137
+ const sent = countEmailRows(params.logs).sent;
137
138
  const delivered = params.logs.filter(isDeliveredRow).length;
138
139
  const trueRows = params.attributionRows.filter(row => isTrueConversionEvent(row.conversion_event));
139
140
  const campaignDenominators = new Map();
140
141
  for (const row of params.logs) {
141
- if (typeof row.campaign_id !== 'string')
142
+ if (typeof row.campaign_id !== 'string' || !isSentRow(row))
142
143
  continue;
143
144
  campaignDenominators.set(row.campaign_id, (campaignDenominators.get(row.campaign_id) ?? 0) + 1);
144
145
  }
@@ -815,7 +816,7 @@ export function registerAnalyticsTools(server) {
815
816
  .eq('site_id', site_id)
816
817
  .gte('sent_at', sevenDaysAgo);
817
818
  const logs = recentLogs ?? [];
818
- const totalSent = logs.length;
819
+ const totalSent = countEmailRows(logs).sent;
819
820
  const bounced = logs.filter((l) => isBouncedRow(l)).length;
820
821
  const complained = logs.filter(isComplainedRow).length;
821
822
  const bounceRate = percentRate(bounced, totalSent, 8) ?? 0;
@@ -1012,7 +1013,7 @@ export function registerAnalyticsTools(server) {
1012
1013
  if (firstHalf.length > 0 && secondHalf.length > 0) {
1013
1014
  trend.open_rate = `${(parseRate(secondMetrics.open_rate) - parseRate(firstMetrics.open_rate) >= 0 ? '+' : '')}${(parseRate(secondMetrics.open_rate) - parseRate(firstMetrics.open_rate)).toFixed(1)}pp`;
1014
1015
  trend.click_rate = `${(parseRate(secondMetrics.click_rate) - parseRate(firstMetrics.click_rate) >= 0 ? '+' : '')}${(parseRate(secondMetrics.click_rate) - parseRate(firstMetrics.click_rate)).toFixed(1)}pp`;
1015
- trend.volume = `${firstHalf.length} → ${secondHalf.length} emails`;
1016
+ trend.volume = `${firstMetrics.total_sent} → ${secondMetrics.total_sent} emails`;
1016
1017
  }
1017
1018
  return j({
1018
1019
  period: `Last ${days} days`,
@@ -1077,7 +1078,7 @@ export function registerAnalyticsTools(server) {
1077
1078
  .eq('project_id', proj.id)
1078
1079
  .gte('sent_at', since);
1079
1080
  const rows = logs ?? [];
1080
- const sent = rows.length;
1081
+ const sent = countEmailRows(rows).sent;
1081
1082
  const delivered = rows.filter(isDeliveredRow).length;
1082
1083
  const opened = countPeople(rows, isOpenedRow);
1083
1084
  const clicked = countPeople(rows, isClickedRow);
@@ -1,3 +1,4 @@
1
+ import { readCampaignMetricGroups, SENT_STATUSES } from "@sendinel/domain";
1
2
  import { resolveCampaignReentry, readCampaignPeriods, campaignPeriodFields } from "@sendinel/domain";
2
3
  import { CAMPAIGN_BRANCH_CONDITIONS, campaignBranchConditionValue, isContactPropertyConditionList, STEP_PROPERTY_CONDITIONS_ERROR, validateStepPropertyRules } from "@sendinel/domain";
3
4
  import { z } from 'zod';
@@ -39,7 +40,6 @@ const reentryFields = {
39
40
  reentry_mode: z.enum(['once_ever', 'per_event_period']).optional().describe(REENTRY_DESCRIPTION),
40
41
  period_key: z.string().min(1).max(64).nullable().optional().describe('Flat event-data key naming the period (starts with a letter; letters, digits and underscores only). Key alone selects per_event_period; null clears to once ever. ' + REENTRY_DESCRIPTION),
41
42
  };
42
- const SENT_EMAIL_STATUSES = ['sent', 'delivered', 'opened', 'clicked', 'bounced', 'complained', 'unsubscribed'];
43
43
  function stripHtml(value) {
44
44
  return value
45
45
  .replace(/<style[\s\S]*?<\/style>/gi, ' ')
@@ -758,7 +758,7 @@ export function registerCampaignTools(server) {
758
758
  fires_on: buildCampaignFiresOn(row),
759
759
  trigger_diagnostics: buildCampaignTriggerDiagnostics(row),
760
760
  ...campaignPeriodFields(periods, period),
761
- ...(periods?.totals ? { total_sent: periods.totals.sent } : {}),
761
+ total_sent: periods?.totals?.sent ?? (engagement.ok ? engagement.sent.get(row.id) ?? 0 : null),
762
762
  unique_opened: periods?.totals?.opened ?? (engagement.ok ? engagement.opened.get(row.id) ?? 0 : null),
763
763
  unique_clicked: periods?.totals?.clicked ?? (engagement.ok ? engagement.clicked.get(row.id) ?? 0 : null),
764
764
  };
@@ -1157,7 +1157,7 @@ export function registerCampaignTools(server) {
1157
1157
  .eq('project_id', projectId)
1158
1158
  .eq('campaign_id', campaign_id)
1159
1159
  .eq('contact_id', contact_id)
1160
- .not('status', 'eq', 'failed'),
1160
+ .in('status', SENT_STATUSES),
1161
1161
  enrollment.current_step == null
1162
1162
  ? Promise.resolve({ data: null, error: null })
1163
1163
  : db()
@@ -3746,7 +3746,7 @@ Return ONLY valid JSON with this exact shape:
3746
3746
  .select('id', { count: 'exact', head: true })
3747
3747
  .eq('project_id', projectId)
3748
3748
  .eq('campaign_id', campaign_id)
3749
- .in('status', SENT_EMAIL_STATUSES),
3749
+ .in('status', SENT_STATUSES),
3750
3750
  db()
3751
3751
  .from('email_log')
3752
3752
  .select('id', { count: 'exact', head: true })
@@ -3775,6 +3775,9 @@ Return ONLY valid JSON with this exact shape:
3775
3775
  const visibilityError = stepsError ?? enrollError ?? sentError ?? queuedEmailError ?? failedSendError ?? queueError ?? dlqError;
3776
3776
  if (visibilityError)
3777
3777
  return j({ ...dbToolError(visibilityError), code: 'CAMPAIGN_VISIBILITY_QUERY_FAILED' });
3778
+ const stepMetrics = await readCampaignMetricGroups(db(), projectId, (steps ?? []).map((step) => step.id), "step_id");
3779
+ if (!stepMetrics.ok)
3780
+ return j({ ...dbToolError(stepMetrics.error), code: 'CAMPAIGN_VISIBILITY_QUERY_FAILED' });
3778
3781
  const periods = await readCampaignPeriods(db(), projectId, campaign);
3779
3782
  const outstandingRows = queueRows ?? [];
3780
3783
  const pendingOrRetryRows = outstandingRows.filter((row) => row.status !== 'processing');
@@ -3795,7 +3798,7 @@ Return ONLY valid JSON with this exact shape:
3795
3798
  total_sent: periods?.totals?.sent ?? sentCount ?? 0,
3796
3799
  ...(periods?.totals ? { unique_opened: periods.totals.opened, unique_clicked: periods.totals.clicked } : {}),
3797
3800
  ...campaignPeriodFields(periods, period),
3798
- steps: steps ?? [],
3801
+ steps: (steps ?? []).map((step) => ({ ...step, total_sent: stepMetrics.sent.get(step.id) ?? 0 })),
3799
3802
  active_enrollments: enrollCount ?? 0,
3800
3803
  send_incomplete: queuedCount > 0 || failedCount > 0 || outstandingRows.length > 0 || unresolvedCount > 0,
3801
3804
  queued_email_count: queuedCount,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.147",
3
+ "version": "1.0.149",
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",