@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.
- package/build/tools/advisor.js +11 -10
- package/build/tools/analytics.d.ts +1 -0
- package/build/tools/analytics.js +10 -9
- package/package.json +1 -1
package/build/tools/advisor.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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;
|
package/build/tools/analytics.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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",
|