@sendinel/mcp-server 1.0.71 → 1.0.72

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,3 +1,4 @@
1
+ import { isBouncedRow } from "@sendinel/domain";
1
2
  import { z } from 'zod';
2
3
  import Anthropic from '@anthropic-ai/sdk';
3
4
  import { db } from '../db.js';
@@ -14,7 +15,7 @@ export function computeMetrics(rows) {
14
15
  const delivered = rows.filter((r) => ['delivered', 'opened', 'clicked'].includes(r.status)).length;
15
16
  const opened = rows.filter((r) => ['opened', 'clicked'].includes(r.status)).length;
16
17
  const clicked = rows.filter((r) => r.status === 'clicked').length;
17
- const bounced = rows.filter((r) => r.status === 'bounced').length;
18
+ const bounced = rows.filter(isBouncedRow).length;
18
19
  const complained = rows.filter((r) => r.status === 'complained').length;
19
20
  return {
20
21
  sent,
@@ -78,14 +79,14 @@ export function registerAdvisorTools(server) {
78
79
  const recommendations = [];
79
80
  let skipped = 0;
80
81
  for (const campaign of campaigns ?? []) {
81
- const rows = await readAllRows(db().from('email_log').select('status')
82
+ const rows = await readAllRows(db().from('email_log').select("status, bounced_at")
82
83
  .eq('project_id', projectId).eq('campaign_id', campaign.id).gte('created_at', since));
83
84
  const sends = rows.filter((row) => ['sent', 'delivered', 'opened', 'clicked', 'bounced', 'complained'].includes(row.status)).length;
84
85
  if (sends < 100) {
85
86
  skipped++;
86
87
  continue;
87
88
  }
88
- const bounces = rows.filter((row) => row.status === 'bounced').length;
89
+ const bounces = rows.filter(isBouncedRow).length;
89
90
  const bounceRate = sends ? Number(((bounces / sends) * 100).toFixed(1)) : 0;
90
91
  recommendations.push({
91
92
  campaign_id: campaign.id,
@@ -128,7 +129,7 @@ export function registerAdvisorTools(server) {
128
129
  // 1. Current period email_log
129
130
  let currentQuery = db()
130
131
  .from('email_log')
131
- .select('status, created_at')
132
+ .select("status, created_at, bounced_at")
132
133
  .eq('project_id', projectId)
133
134
  .gte('created_at', periodStart.toISOString());
134
135
  if (site_id)
@@ -137,7 +138,7 @@ export function registerAdvisorTools(server) {
137
138
  // 2. Prior period
138
139
  let priorQuery = db()
139
140
  .from('email_log')
140
- .select('status, created_at')
141
+ .select("status, created_at, bounced_at")
141
142
  .eq('project_id', projectId)
142
143
  .gte('created_at', priorStart.toISOString())
143
144
  .lt('created_at', periodStart.toISOString());
@@ -325,7 +326,7 @@ export function registerAdvisorTools(server) {
325
326
  // 2. Top-performing historical subjects
326
327
  let logQuery = db()
327
328
  .from('email_log')
328
- .select('subject, status')
329
+ .select("subject, status, bounced_at")
329
330
  .eq('project_id', projectId)
330
331
  .in('status', ['delivered', 'opened', 'clicked']);
331
332
  if (site_id)
@@ -447,7 +448,7 @@ Each subject should be distinct in approach (urgency, curiosity, benefit, social
447
448
  // 1. Query email_log with campaign step subjects
448
449
  const logRows = await readAllRows(db()
449
450
  .from('email_log')
450
- .select('campaign_id, subject, status')
451
+ .select("campaign_id, subject, status, bounced_at")
451
452
  .eq('project_id', projectId)
452
453
  .eq('site_id', site_id)
453
454
  .gte('created_at', since)
@@ -583,7 +584,7 @@ Return ONLY valid JSON (no markdown fences): { "topics": [{ "title": "...", "rat
583
584
  // 2. Query email_log
584
585
  let query = db()
585
586
  .from('email_log')
586
- .select('status, created_at')
587
+ .select("status, created_at, bounced_at")
587
588
  .eq('project_id', projectId)
588
589
  .eq('site_id', site_id)
589
590
  .gte('created_at', since)
@@ -667,13 +668,13 @@ Return ONLY valid JSON (no markdown fences): { "topics": [{ "title": "...", "rat
667
668
  const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
668
669
  const { data: recentLogs } = await db()
669
670
  .from('email_log')
670
- .select('status, opened_at, clicked_at')
671
+ .select("status, opened_at, clicked_at, bounced_at")
671
672
  .eq('project_id', projectId)
672
673
  .eq('site_id', site_id)
673
674
  .gte('sent_at', sevenDaysAgo);
674
675
  const logs = recentLogs ?? [];
675
676
  const totalSent = logs.length;
676
- const bounced = logs.filter((l) => l.status === 'bounced').length;
677
+ const bounced = logs.filter(isBouncedRow).length;
677
678
  const complained = logs.filter((l) => l.status === 'complained').length;
678
679
  const delivered = logs.filter((l) => ['delivered', 'opened', 'clicked'].includes(l.status)).length;
679
680
  const opened = logs.filter((l) => l.opened_at).length;
@@ -1,5 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  type EmailLogRow = {
3
+ bounced_at: string | null;
3
4
  id?: string;
4
5
  provider_message_id?: string | null;
5
6
  status: string;
@@ -1,3 +1,4 @@
1
+ import { isBouncedRow } from "@sendinel/domain";
1
2
  import { withApproval } from '../lib/action-approvals.js';
2
3
  import { fetchAllRows, readAllRows } from '@sendinel/domain';
3
4
  import { z } from 'zod';
@@ -78,7 +79,7 @@ export function computeAnalyticsMetrics(rows, eventCounts) {
78
79
  const delivered = rows.filter(r => ['delivered', 'opened', 'clicked'].includes(r.status)).length;
79
80
  const opened = eventCounts?.opened ?? rows.filter(r => r.status === 'opened' || r.status === 'clicked').length;
80
81
  const clicked = eventCounts?.clicked ?? rows.filter(r => r.status === 'clicked').length;
81
- const bounced = rows.filter(r => r.status === 'bounced').length;
82
+ const bounced = rows.filter(isBouncedRow).length;
82
83
  const complained = rows.filter(r => r.status === 'complained').length;
83
84
  const unsubscribed = rows.filter(r => r.status === 'unsubscribed').length;
84
85
  return {
@@ -326,7 +327,7 @@ export function registerAnalyticsTools(server) {
326
327
  const since = new Date(Date.now() - period_days * 86400000).toISOString();
327
328
  let rows;
328
329
  try {
329
- rows = await readAllRows(db().from('email_log').select('site_id, status, sent_at, created_at')
330
+ rows = await readAllRows(db().from('email_log').select("site_id, status, sent_at, created_at, bounced_at")
330
331
  .eq('project_id', getProjectId()).gte('created_at', since));
331
332
  }
332
333
  catch (readError) {
@@ -547,7 +548,7 @@ export function registerAnalyticsTools(server) {
547
548
  const trackingEnabled = site?.tracking_enabled ?? false;
548
549
  const { data, error, pageCount: logPageCount } = await fetchAllRows(async (offset, limit) => db()
549
550
  .from('email_log')
550
- .select('id, provider_message_id, status, sent_at')
551
+ .select("id, provider_message_id, status, sent_at, bounced_at")
551
552
  .eq('project_id', projectId)
552
553
  .eq('site_id', site_id)
553
554
  .gte('sent_at', `${start_date}T00:00:00Z`)
@@ -700,7 +701,7 @@ export function registerAnalyticsTools(server) {
700
701
  const since7d = new Date(Date.now() - 7 * 86_400_000).toISOString();
701
702
  let q = db()
702
703
  .from('email_log')
703
- .select('status, opened_at, clicked_at, sent_at')
704
+ .select("status, opened_at, clicked_at, sent_at, bounced_at")
704
705
  .eq('project_id', projectId)
705
706
  .gte('sent_at', since30d);
706
707
  if (site_id)
@@ -873,13 +874,13 @@ export function registerAnalyticsTools(server) {
873
874
  const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
874
875
  const { data: recentLogs } = await db()
875
876
  .from('email_log')
876
- .select('status')
877
+ .select("status, bounced_at")
877
878
  .eq('project_id', projectId)
878
879
  .eq('site_id', site_id)
879
880
  .gte('sent_at', sevenDaysAgo);
880
881
  const logs = recentLogs ?? [];
881
882
  const totalSent = logs.length;
882
- const bounced = logs.filter((l) => l.status === 'bounced').length;
883
+ const bounced = logs.filter((l) => isBouncedRow(l)).length;
883
884
  const complained = logs.filter((l) => l.status === 'complained').length;
884
885
  const bounceRate = totalSent > 0 ? (bounced / totalSent * 100) : 0;
885
886
  const complaintRate = totalSent > 0 ? (complained / totalSent * 100) : 0;
@@ -969,7 +970,7 @@ export function registerAnalyticsTools(server) {
969
970
  const delivered = set.filter(r => ['delivered', 'opened', 'clicked'].includes(r.status)).length;
970
971
  const opened = set.filter(r => r.opened_at).length;
971
972
  const clicked = set.filter(r => r.clicked_at).length;
972
- const bounced = set.filter(r => r.status === 'bounced').length;
973
+ const bounced = set.filter(isBouncedRow).length;
973
974
  const complained = set.filter(r => r.status === 'complained').length;
974
975
  return {
975
976
  total_sent: total, delivered, opened, clicked, bounced, complained,
@@ -1143,7 +1144,7 @@ export function registerAnalyticsTools(server) {
1143
1144
  const projDb = client ?? adminDb();
1144
1145
  const { data: logs } = await projDb
1145
1146
  .from('email_log')
1146
- .select('status, opened_at, clicked_at')
1147
+ .select("status, opened_at, clicked_at, bounced_at")
1147
1148
  .eq('project_id', proj.id)
1148
1149
  .gte('sent_at', since);
1149
1150
  const rows = logs ?? [];
@@ -1151,7 +1152,7 @@ export function registerAnalyticsTools(server) {
1151
1152
  const delivered = rows.filter((r) => ['delivered', 'opened', 'clicked'].includes(r.status)).length;
1152
1153
  const opened = rows.filter((r) => r.opened_at).length;
1153
1154
  const clicked = rows.filter((r) => r.clicked_at).length;
1154
- const bounced = rows.filter((r) => r.status === 'bounced').length;
1155
+ const bounced = rows.filter(isBouncedRow).length;
1155
1156
  const complained = rows.filter((r) => r.status === 'complained').length;
1156
1157
  results.push({
1157
1158
  project_id: proj.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.71",
3
+ "version": "1.0.72",
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",