@sendinel/mcp-server 1.0.3 → 1.0.5

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.
Files changed (60) hide show
  1. package/build/audit.d.ts +1 -0
  2. package/build/audit.js +9 -3
  3. package/build/auth.d.ts +10 -0
  4. package/build/auth.js +82 -9
  5. package/build/db.d.ts +12 -0
  6. package/build/db.js +37 -2
  7. package/build/index.js +38 -12
  8. package/build/lib/action-approvals.d.ts +28 -1
  9. package/build/lib/action-approvals.js +89 -2
  10. package/build/lib/brand-asset-persist.d.ts +35 -0
  11. package/build/lib/brand-asset-persist.js +161 -0
  12. package/build/lib/brand-asset-usage.d.ts +13 -0
  13. package/build/lib/brand-asset-usage.js +46 -0
  14. package/build/lib/email-provider.d.ts +19 -0
  15. package/build/lib/email-provider.js +281 -0
  16. package/build/lib/ssrf-pinned-fetch.d.ts +16 -0
  17. package/build/lib/ssrf-pinned-fetch.js +101 -0
  18. package/build/lib/survey-link-resolver.d.ts +6 -0
  19. package/build/lib/survey-link-resolver.js +23 -0
  20. package/build/lib/svg-sanitize.d.ts +2 -0
  21. package/build/lib/svg-sanitize.js +110 -0
  22. package/build/lib/webhook-events.d.ts +1 -1
  23. package/build/scoped-token.d.ts +11 -0
  24. package/build/scoped-token.js +59 -0
  25. package/build/server.js +30 -9
  26. package/build/tools/ab-testing.js +28 -0
  27. package/build/tools/advisor.js +52 -2
  28. package/build/tools/ai-work.d.ts +2 -0
  29. package/build/tools/ai-work.js +460 -0
  30. package/build/tools/analytics.d.ts +1 -0
  31. package/build/tools/analytics.js +203 -0
  32. package/build/tools/approvals.js +15 -1
  33. package/build/tools/automations.js +288 -65
  34. package/build/tools/brand-kit.d.ts +2 -0
  35. package/build/tools/brand-kit.js +277 -0
  36. package/build/tools/campaigns.js +900 -69
  37. package/build/tools/compound.js +28 -0
  38. package/build/tools/contacts.js +338 -17
  39. package/build/tools/content.js +266 -1
  40. package/build/tools/data-proposals.js +21 -0
  41. package/build/tools/data.js +121 -5
  42. package/build/tools/delivery-ops.js +84 -0
  43. package/build/tools/drafts.js +64 -1
  44. package/build/tools/forms.js +42 -9
  45. package/build/tools/gdpr.js +22 -3
  46. package/build/tools/newsletters.d.ts +2 -0
  47. package/build/tools/newsletters.js +583 -0
  48. package/build/tools/org.js +297 -3
  49. package/build/tools/segments.js +45 -3
  50. package/build/tools/short-links.d.ts +2 -0
  51. package/build/tools/short-links.js +157 -0
  52. package/build/tools/sites.js +35 -0
  53. package/build/tools/sms.js +49 -0
  54. package/build/tools/social-posts.js +81 -5
  55. package/build/tools/surveys.d.ts +2 -0
  56. package/build/tools/surveys.js +222 -0
  57. package/build/tools/templates.js +78 -2
  58. package/build/tools/warmup.js +28 -0
  59. package/build/tools/webhooks.js +29 -0
  60. package/package.json +10 -4
@@ -147,8 +147,85 @@ function distinctEventCount(events, eventType, logIds) {
147
147
  .filter(event => event.event_type === eventType && event.email_log_id && logIds.has(event.email_log_id))
148
148
  .map(event => event.email_log_id)).size;
149
149
  }
150
+ function isTrueConversionEvent(event) {
151
+ const normalized = (event ?? '').trim().toLowerCase().replace(/[\s-]+/g, '_');
152
+ if (!normalized)
153
+ return false;
154
+ return !['open', 'opened', 'email_open', 'click', 'clicked', 'email_click', 'page_view', 'view', 'impression'].includes(normalized);
155
+ }
156
+ function buildTrueConversionAnalytics(params) {
157
+ const sent = params.logs.length;
158
+ const delivered = params.logs.filter(row => ['delivered', 'opened', 'clicked'].includes(row.status)).length;
159
+ const trueRows = params.attributionRows.filter(row => isTrueConversionEvent(row.conversion_event));
160
+ const campaignDenominators = new Map();
161
+ for (const row of params.logs) {
162
+ if (typeof row.campaign_id !== 'string')
163
+ continue;
164
+ campaignDenominators.set(row.campaign_id, (campaignDenominators.get(row.campaign_id) ?? 0) + 1);
165
+ }
166
+ const byEvent = new Map();
167
+ const byVariant = new Map();
168
+ let totalRevenue = 0;
169
+ for (const row of trueRows) {
170
+ const revenue = Number(row.revenue_amount ?? 0) || 0;
171
+ totalRevenue += revenue;
172
+ const event = row.conversion_event?.trim() || 'conversion';
173
+ const eventStats = byEvent.get(event) ?? { conversion_event: event, count: 0, revenue: 0 };
174
+ eventStats.count += 1;
175
+ eventStats.revenue += revenue;
176
+ byEvent.set(event, eventStats);
177
+ const variant = row.utm_content?.trim() || '(none)';
178
+ const variantKey = `${row.campaign_id ?? '(none)'}:${variant}`;
179
+ const variantStats = byVariant.get(variantKey) ?? { variant, campaign_id: row.campaign_id ?? null, count: 0, revenue: 0 };
180
+ variantStats.count += 1;
181
+ variantStats.revenue += revenue;
182
+ byVariant.set(variantKey, variantStats);
183
+ }
184
+ const percentOf = (count, denominator) => denominator > 0 ? `${((count / denominator) * 100).toFixed(1)}%` : 'N/A';
185
+ const stage = (name, count, prior) => ({
186
+ stage: name,
187
+ count,
188
+ rate_from_sent: percentOf(count, sent),
189
+ rate_from_previous: prior == null ? '100.0%' : percentOf(count, prior),
190
+ drop_off_from_previous: prior == null ? 0 : Math.max(0, prior - count),
191
+ });
192
+ return {
193
+ definition: 'revenue_attribution rows excluding open/click/page-view vanity events',
194
+ true_conversions: trueRows.length,
195
+ true_conversion_rate: percentOf(trueRows.length, sent),
196
+ revenue: Number(totalRevenue.toFixed(2)),
197
+ by_event: Array.from(byEvent.values())
198
+ .map(row => ({ ...row, revenue: Number(row.revenue.toFixed(2)), true_conversion_rate: percentOf(row.count, sent) }))
199
+ .sort((a, b) => b.count - a.count),
200
+ funnel: [
201
+ stage('sent', sent, null),
202
+ stage('delivered', delivered, sent),
203
+ stage('opened', params.opened, delivered),
204
+ stage('clicked', params.clicked, params.opened),
205
+ stage('true_conversion', trueRows.length, params.clicked || params.opened || delivered),
206
+ ],
207
+ by_variant: Array.from(byVariant.values())
208
+ .map(row => {
209
+ const denominator = row.campaign_id ? (campaignDenominators.get(row.campaign_id) ?? sent) : sent;
210
+ return {
211
+ ...row,
212
+ revenue: Number(row.revenue.toFixed(2)),
213
+ denominator,
214
+ true_conversion_rate: percentOf(row.count, denominator),
215
+ };
216
+ })
217
+ .sort((a, b) => b.count - a.count || b.revenue - a.revenue),
218
+ };
219
+ }
150
220
  export function registerAnalyticsTools(server) {
151
221
  server.registerTool('list_event_log', {
222
+ title: 'List Event Log',
223
+ annotations: {
224
+ readOnlyHint: true,
225
+ destructiveHint: false,
226
+ idempotentHint: true,
227
+ openWorldHint: false,
228
+ },
152
229
  description: 'List received API event tracking calls with optional filters.',
153
230
  inputSchema: {
154
231
  event_name: z.string().optional(),
@@ -177,6 +254,13 @@ export function registerAnalyticsTools(server) {
177
254
  return j({ events: data ?? [] });
178
255
  });
179
256
  server.registerTool('get_portfolio_analytics', {
257
+ title: 'Get Portfolio Analytics',
258
+ annotations: {
259
+ readOnlyHint: true,
260
+ destructiveHint: false,
261
+ idempotentHint: true,
262
+ openWorldHint: false,
263
+ },
180
264
  description: 'Get cross-site analytics totals and per-site breakdown.',
181
265
  inputSchema: { period_days: z.number().int().min(1).max(365).optional().default(30) },
182
266
  }, async ({ period_days = 30 }) => {
@@ -197,6 +281,13 @@ export function registerAnalyticsTools(server) {
197
281
  });
198
282
  });
199
283
  server.registerTool('list_projects', {
284
+ title: 'List Projects',
285
+ annotations: {
286
+ readOnlyHint: true,
287
+ destructiveHint: false,
288
+ idempotentHint: true,
289
+ openWorldHint: false,
290
+ },
200
291
  description: 'List all projects accessible to this API key. Returns project ID, name, plan, and associated organization. Useful for orchestrators managing multiple products through a single Sendinel account.',
201
292
  inputSchema: {},
202
293
  }, async () => {
@@ -238,6 +329,13 @@ export function registerAnalyticsTools(server) {
238
329
  });
239
330
  });
240
331
  server.registerTool('get_sites', {
332
+ title: 'Get Sites',
333
+ annotations: {
334
+ readOnlyHint: true,
335
+ destructiveHint: false,
336
+ idempotentHint: true,
337
+ openWorldHint: false,
338
+ },
241
339
  description: 'List all active Sendinel sites',
242
340
  inputSchema: {},
243
341
  }, async () => {
@@ -252,6 +350,13 @@ export function registerAnalyticsTools(server) {
252
350
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
253
351
  });
254
352
  server.registerTool('get_stats', {
353
+ title: 'Get Stats',
354
+ annotations: {
355
+ readOnlyHint: true,
356
+ destructiveHint: false,
357
+ idempotentHint: true,
358
+ openWorldHint: false,
359
+ },
255
360
  description: 'Aggregate email metrics for a site over a date range. Returns sent, delivered, bounce/complaint rates, Sender Health Score (0–100), send volume by day, list growth rate, inactive subscriber percentage, and open/click rates when tracking is enabled.',
256
361
  inputSchema: {
257
362
  site_id: z.string().uuid().describe('Site UUID'),
@@ -338,6 +443,13 @@ export function registerAnalyticsTools(server) {
338
443
  return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
339
444
  });
340
445
  server.registerTool('get_domain_health', {
446
+ title: 'Get Domain Health',
447
+ annotations: {
448
+ readOnlyHint: true,
449
+ destructiveHint: false,
450
+ idempotentHint: true,
451
+ openWorldHint: true,
452
+ },
341
453
  description: 'SPF/DKIM/DMARC status and warmup progress for a site',
342
454
  inputSchema: {
343
455
  site_id: z.string().uuid().describe('Site UUID'),
@@ -355,6 +467,13 @@ export function registerAnalyticsTools(server) {
355
467
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
356
468
  });
357
469
  server.registerTool('get_engagement_insights', {
470
+ title: 'Get Engagement Insights',
471
+ annotations: {
472
+ readOnlyHint: true,
473
+ destructiveHint: false,
474
+ idempotentHint: true,
475
+ openWorldHint: false,
476
+ },
358
477
  description: 'Get engagement scoring summary: lifecycle distribution, score buckets, top engaged contacts, at-risk contacts, and never-engaged counts. Use this data to recommend re-engagement campaigns, identify high-value contacts, or suggest list hygiene actions.',
359
478
  inputSchema: {
360
479
  site_id: z.string().uuid().optional().describe('Optional site UUID for site-specific tracking settings and engagement anomaly analysis'),
@@ -447,6 +566,13 @@ export function registerAnalyticsTools(server) {
447
566
  }, null, 2) }] };
448
567
  });
449
568
  server.registerTool('deliverability_check', {
569
+ title: 'Deliverability Check',
570
+ annotations: {
571
+ readOnlyHint: true,
572
+ destructiveHint: false,
573
+ idempotentHint: true,
574
+ openWorldHint: true,
575
+ },
450
576
  description: 'Run a deliverability check for a site: DNS records (SPF/DKIM/DMARC), warmup status, bounce/complaint rates, and risk assessment',
451
577
  inputSchema: {
452
578
  site_id: z.string().uuid(),
@@ -601,6 +727,13 @@ export function registerAnalyticsTools(server) {
601
727
  });
602
728
  });
603
729
  server.registerTool('performance_report', {
730
+ title: 'Performance Report',
731
+ annotations: {
732
+ readOnlyHint: true,
733
+ destructiveHint: false,
734
+ idempotentHint: true,
735
+ openWorldHint: false,
736
+ },
604
737
  description: 'Generate a performance report: delivery/open/click rates, best send times, trends. Optionally group by campaign, site, company, day, or week.',
605
738
  inputSchema: {
606
739
  days: z.number().int().min(1).max(365).optional().describe('Lookback period in days, default 30'),
@@ -626,6 +759,20 @@ export function registerAnalyticsTools(server) {
626
759
  const rows = logs ?? [];
627
760
  if (!rows.length)
628
761
  return j({ message: `No email activity in the last ${days} days.`, total: 0 });
762
+ let attributionQuery = db()
763
+ .from('revenue_attribution')
764
+ .select('campaign_id, conversion_event, utm_content, revenue_amount')
765
+ .eq('project_id', projectId)
766
+ .gte('converted_at', since);
767
+ if (campaign_id)
768
+ attributionQuery = attributionQuery.eq('campaign_id', campaign_id);
769
+ const { data: attributionRows, error: attributionError } = await attributionQuery;
770
+ if (attributionError)
771
+ return txt(`Error: ${attributionError.message}`);
772
+ const campaignIdsInScope = new Set(rows.map((row) => typeof row.campaign_id === 'string' ? row.campaign_id : null).filter(Boolean));
773
+ const scopedAttributionRows = site_id
774
+ ? (attributionRows ?? []).filter((row) => row.campaign_id && campaignIdsInScope.has(row.campaign_id))
775
+ : (attributionRows ?? []);
629
776
  const computeMetrics = (set) => {
630
777
  const total = set.length;
631
778
  const delivered = set.filter(r => ['delivered', 'opened', 'clicked'].includes(r.status)).length;
@@ -644,6 +791,12 @@ export function registerAnalyticsTools(server) {
644
791
  };
645
792
  };
646
793
  const overall = computeMetrics(rows);
794
+ const trueConversionAnalytics = buildTrueConversionAnalytics({
795
+ logs: rows,
796
+ attributionRows: scopedAttributionRows,
797
+ opened: parseInt(String(overall.opened), 10) || 0,
798
+ clicked: parseInt(String(overall.clicked), 10) || 0,
799
+ });
647
800
  const hourBuckets = {};
648
801
  rows.forEach((r) => {
649
802
  if (!r.sent_at)
@@ -744,6 +897,7 @@ export function registerAnalyticsTools(server) {
744
897
  return j({
745
898
  period: `Last ${days} days`,
746
899
  overall,
900
+ true_conversion_analytics: trueConversionAnalytics,
747
901
  best_send_times_utc: bestHours,
748
902
  trend_vs_prior_period: trend,
749
903
  ...(grouped ? { [`by_${group_by}`]: grouped } : {}),
@@ -751,6 +905,13 @@ export function registerAnalyticsTools(server) {
751
905
  });
752
906
  // ── Cross-project analytics ─────────────────────────────────────────────
753
907
  server.registerTool('get_portfolio_stats', {
908
+ title: 'Get Portfolio Stats',
909
+ annotations: {
910
+ readOnlyHint: true,
911
+ destructiveHint: false,
912
+ idempotentHint: true,
913
+ openWorldHint: false,
914
+ },
754
915
  description: 'Aggregate email metrics across all projects in your organization. Returns per-project and combined totals for the specified period. Useful for orchestrators managing multiple products.',
755
916
  inputSchema: {
756
917
  days: z.number().int().min(1).max(90).optional().default(30).describe('Lookback period in days (default: 30)'),
@@ -843,6 +1004,13 @@ export function registerAnalyticsTools(server) {
843
1004
  });
844
1005
  });
845
1006
  server.registerTool('abuse_monitor_status', {
1007
+ title: 'Abuse Monitor Status',
1008
+ annotations: {
1009
+ readOnlyHint: true,
1010
+ destructiveHint: false,
1011
+ idempotentHint: true,
1012
+ openWorldHint: false,
1013
+ },
846
1014
  description: 'Return per-tenant abuse monitor metrics for a site, including complaint rate and pause level.',
847
1015
  inputSchema: {
848
1016
  siteId: z.string().uuid(),
@@ -880,6 +1048,13 @@ export function registerAnalyticsTools(server) {
880
1048
  });
881
1049
  });
882
1050
  server.registerTool('abuse_monitor_override', {
1051
+ title: 'Abuse Monitor Override',
1052
+ annotations: {
1053
+ readOnlyHint: false,
1054
+ destructiveHint: false,
1055
+ idempotentHint: false,
1056
+ openWorldHint: false,
1057
+ },
883
1058
  description: 'Create a temporary abuse monitor override for a paused site. Overrides expire automatically and are capped at 72 hours.',
884
1059
  inputSchema: {
885
1060
  siteId: z.string().uuid(),
@@ -927,6 +1102,13 @@ export function registerAnalyticsTools(server) {
927
1102
  });
928
1103
  });
929
1104
  server.registerTool('get_site_insights', {
1105
+ title: 'Get Site Insights',
1106
+ annotations: {
1107
+ readOnlyHint: true,
1108
+ destructiveHint: false,
1109
+ idempotentHint: true,
1110
+ openWorldHint: false,
1111
+ },
930
1112
  description: 'Get pre-computed AI insights for a site — anomalies, deliverability alerts, send-time recommendations, and attribution summaries. Call this before making campaign decisions to check for active warnings.',
931
1113
  inputSchema: {
932
1114
  site_id: z.string().uuid().describe('Site UUID'),
@@ -960,6 +1142,13 @@ export function registerAnalyticsTools(server) {
960
1142
  return j((data ?? []).sort((a, b) => (rank[a.severity] ?? 9) - (rank[b.severity] ?? 9)));
961
1143
  });
962
1144
  server.registerTool('create_social_campaign', {
1145
+ title: 'Create Social Campaign',
1146
+ annotations: {
1147
+ readOnlyHint: false,
1148
+ destructiveHint: false,
1149
+ idempotentHint: false,
1150
+ openWorldHint: false,
1151
+ },
963
1152
  description: 'Create a social media campaign with a UTM-tracked link. Returns the tracked URL to paste into the social post.',
964
1153
  inputSchema: {
965
1154
  name: z.string(),
@@ -993,6 +1182,13 @@ export function registerAnalyticsTools(server) {
993
1182
  return j({ id: data.id, tracked_url: data.tracked_url, campaign: data });
994
1183
  });
995
1184
  server.registerTool('list_social_campaigns', {
1185
+ title: 'List Social Campaigns',
1186
+ annotations: {
1187
+ readOnlyHint: true,
1188
+ destructiveHint: false,
1189
+ idempotentHint: true,
1190
+ openWorldHint: false,
1191
+ },
996
1192
  description: 'List social campaigns with their signup attribution and manual metrics. Use to compare channel performance.',
997
1193
  inputSchema: {
998
1194
  site_id: z.string().uuid().optional(),
@@ -1016,6 +1212,13 @@ export function registerAnalyticsTools(server) {
1016
1212
  return j({ campaigns: data ?? [] });
1017
1213
  });
1018
1214
  server.registerTool('schedule_social_post', {
1215
+ title: 'Schedule Social Post',
1216
+ annotations: {
1217
+ readOnlyHint: false,
1218
+ destructiveHint: true,
1219
+ idempotentHint: false,
1220
+ openWorldHint: true,
1221
+ },
1019
1222
  description: 'Schedule a social campaign post via a connected BYO publisher (UploadPost, Buffer). Returns publisherPostId on success.',
1020
1223
  inputSchema: {
1021
1224
  campaign_id: z.string().uuid().describe('UUID of the social campaign in Sendinel'),
@@ -6,6 +6,13 @@ const txt = (text) => ({ content: [{ type: 'text', text }] });
6
6
  const j = (data) => txt(JSON.stringify(data, null, 2));
7
7
  export function registerApprovalTools(server) {
8
8
  server.registerTool('check_approval', {
9
+ title: 'Check Approval',
10
+ annotations: {
11
+ readOnlyHint: true,
12
+ destructiveHint: false,
13
+ idempotentHint: true,
14
+ openWorldHint: false,
15
+ },
9
16
  description: 'Check the status of a pending action approval. Use this to poll after requesting an action that requires human approval. Returns approved/pending/rejected/expired.',
10
17
  inputSchema: {
11
18
  approval_id: z.string().uuid().describe('The approval ID returned by the original tool call'),
@@ -15,12 +22,19 @@ export function registerApprovalTools(server) {
15
22
  return j(result);
16
23
  });
17
24
  server.registerTool('list_pending_approvals', {
25
+ title: 'List Pending Approvals',
26
+ annotations: {
27
+ readOnlyHint: true,
28
+ destructiveHint: false,
29
+ idempotentHint: true,
30
+ openWorldHint: false,
31
+ },
18
32
  description: 'List all pending approval requests for this project. Shows what actions are waiting for human approval.',
19
33
  inputSchema: {},
20
34
  }, async () => {
21
35
  const { data, error } = await db()
22
36
  .from('action_approvals')
23
- .select('id, action, tier, summary, status, requested_at, expires_at')
37
+ .select('id, action, tier, channel, summary, status, requested_at, expires_at')
24
38
  .eq('project_id', getProjectId())
25
39
  .eq('status', 'pending')
26
40
  .order('requested_at', { ascending: false })