@sendinel/mcp-server 1.0.100 → 1.0.101
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/analytics.js +26 -11
- package/build/tools/campaigns.js +20 -9
- package/package.json +1 -1
package/build/tools/analytics.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readPeriodCampaigns, campaignPeriodFields } from "@sendinel/domain";
|
|
1
2
|
import { isTrueConversionEvent, TRUE_CONVERSION_DEFINITION } from "@sendinel/domain";
|
|
2
3
|
import { percentRate, groupMetricRows, readRevenue, isComplainedRow, isOpenedRow, countPeople, isClickedRow, summarizeEmailRows, countEmailRows, isDeliveredRow, isBouncedRow, fetchAllRows, readAllRows } from "@sendinel/domain";
|
|
3
4
|
import { z } from 'zod';
|
|
@@ -851,9 +852,10 @@ export function registerAnalyticsTools(server) {
|
|
|
851
852
|
days: z.number().int().min(1).max(365).optional().describe('Lookback period in days, default 30'),
|
|
852
853
|
site_id: z.string().uuid().optional(),
|
|
853
854
|
campaign_id: z.string().uuid().optional(),
|
|
855
|
+
period: z.string().min(1).optional().describe('Return one enrollment period in by_period; existing totals keep their date window.'),
|
|
854
856
|
group_by: z.enum(['campaign', 'site', 'company', 'day', 'week']).optional(),
|
|
855
857
|
},
|
|
856
|
-
}, async ({ days = 30, site_id, campaign_id, group_by }) => {
|
|
858
|
+
}, async ({ days = 30, site_id, campaign_id, group_by, period }) => {
|
|
857
859
|
const projectId = getProjectId();
|
|
858
860
|
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
859
861
|
let q = db()
|
|
@@ -868,20 +870,28 @@ export function registerAnalyticsTools(server) {
|
|
|
868
870
|
const { data: logs, error } = await q;
|
|
869
871
|
if (error)
|
|
870
872
|
return txt(`Error: ${error.message}`);
|
|
873
|
+
let periodCampaigns;
|
|
874
|
+
try {
|
|
875
|
+
periodCampaigns = campaign_id || group_by === 'campaign' ? await readPeriodCampaigns(db(), projectId, { siteId: site_id, campaignId: campaign_id, start: since }) : new Map();
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
return txt("Error: Could not load results by period.");
|
|
879
|
+
}
|
|
880
|
+
const selectedFields = (id) => campaignPeriodFields(periodCampaigns.get(id)?.result, period);
|
|
871
881
|
const rows = logs ?? [];
|
|
872
|
-
if (!rows.length)
|
|
882
|
+
if (!rows.length && !periodCampaigns.size)
|
|
873
883
|
return j({ message: `No email activity in the last ${days} days.`, total: 0 });
|
|
874
884
|
const { data: attributionRows, error: attributionError } = await readRevenue({ sb: db(), projectId, siteId: site_id, campaignId: campaign_id, start: since });
|
|
875
885
|
if (attributionError)
|
|
876
886
|
return txt(`Error: ${attributionError.message}`);
|
|
877
887
|
const scopedAttributionRows = attributionRows ?? [];
|
|
878
|
-
const computeMetrics = (set) => {
|
|
879
|
-
const total = set.length;
|
|
880
|
-
const delivered = set.filter(isDeliveredRow).length;
|
|
881
|
-
const opened = countPeople(set, isOpenedRow);
|
|
882
|
-
const clicked = countPeople(set, isClickedRow);
|
|
883
|
-
const bounced = set.filter(isBouncedRow).length;
|
|
884
|
-
const complained = set.filter(isComplainedRow).length;
|
|
888
|
+
const computeMetrics = (set, counts) => {
|
|
889
|
+
const total = counts?.sent ?? set.length;
|
|
890
|
+
const delivered = counts?.delivered ?? set.filter(isDeliveredRow).length;
|
|
891
|
+
const opened = counts?.opened ?? countPeople(set, isOpenedRow);
|
|
892
|
+
const clicked = counts?.clicked ?? countPeople(set, isClickedRow);
|
|
893
|
+
const bounced = counts?.bounced ?? set.filter(isBouncedRow).length;
|
|
894
|
+
const complained = counts?.complained ?? set.filter(isComplainedRow).length;
|
|
885
895
|
return {
|
|
886
896
|
total_sent: total, delivered, opened, clicked, bounced, complained,
|
|
887
897
|
delivery_rate: total > 0 ? `${percentRate(delivered, total, 1)?.toFixed(1)}%` : 'N/A',
|
|
@@ -892,7 +902,7 @@ export function registerAnalyticsTools(server) {
|
|
|
892
902
|
click_to_open: opened > 0 ? `${percentRate(clicked, opened, 1)?.toFixed(1)}%` : 'N/A',
|
|
893
903
|
};
|
|
894
904
|
};
|
|
895
|
-
const overall = computeMetrics(rows);
|
|
905
|
+
const overall = computeMetrics(rows, campaign_id ? periodCampaigns.get(campaign_id)?.result.totals : undefined);
|
|
896
906
|
const trueConversionAnalytics = buildTrueConversionAnalytics({
|
|
897
907
|
logs: rows,
|
|
898
908
|
attributionRows: scopedAttributionRows,
|
|
@@ -913,9 +923,13 @@ export function registerAnalyticsTools(server) {
|
|
|
913
923
|
byKey[key] = [];
|
|
914
924
|
byKey[key].push(r);
|
|
915
925
|
});
|
|
926
|
+
if (group_by === 'campaign')
|
|
927
|
+
for (const id of periodCampaigns.keys())
|
|
928
|
+
byKey[id] ??= [];
|
|
916
929
|
grouped = Object.entries(byKey).map(([id, set]) => ({
|
|
917
930
|
[`${group_by}_id`]: id,
|
|
918
|
-
...computeMetrics(set),
|
|
931
|
+
...computeMetrics(set, group_by === 'campaign' ? periodCampaigns.get(id)?.result.totals : undefined),
|
|
932
|
+
...(group_by === 'campaign' ? selectedFields(id) : {}),
|
|
919
933
|
})).sort((a, b) => b.total_sent - a.total_sent);
|
|
920
934
|
}
|
|
921
935
|
else if (group_by === 'company') {
|
|
@@ -987,6 +1001,7 @@ export function registerAnalyticsTools(server) {
|
|
|
987
1001
|
return j({
|
|
988
1002
|
period: `Last ${days} days`,
|
|
989
1003
|
overall,
|
|
1004
|
+
...(campaign_id ? selectedFields(campaign_id) : {}),
|
|
990
1005
|
true_conversion_analytics: trueConversionAnalytics,
|
|
991
1006
|
best_send_times_utc: bestHours,
|
|
992
1007
|
trend_vs_prior_period: trend,
|
package/build/tools/campaigns.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readCampaignPeriods, campaignPeriodFields } from "@sendinel/domain";
|
|
1
2
|
import { CAMPAIGN_BRANCH_CONDITIONS, campaignBranchConditionValue, isContactPropertyConditionList, STEP_PROPERTY_CONDITIONS_ERROR, validateStepPropertyRules } from "@sendinel/domain";
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import Anthropic from '@anthropic-ai/sdk';
|
|
@@ -643,13 +644,14 @@ export function registerCampaignTools(server) {
|
|
|
643
644
|
description: 'List campaigns, optionally filtered by site and/or status',
|
|
644
645
|
inputSchema: {
|
|
645
646
|
site_id: z.string().uuid().optional(),
|
|
647
|
+
period: z.string().min(1).optional().describe("Return one enrollment period in by_period; totals remain all-time."),
|
|
646
648
|
status: z.enum(['draft', 'active', 'paused', 'completed', 'archived']).optional(),
|
|
647
649
|
limit: z.number().int().min(1).max(100).optional().default(25),
|
|
648
650
|
},
|
|
649
|
-
}, async ({ site_id, status, limit }) => {
|
|
651
|
+
}, async ({ site_id, status, limit, period }) => {
|
|
650
652
|
let q = db()
|
|
651
653
|
.from('campaigns')
|
|
652
|
-
.select('id, site_id, name, type, status, sends_per_hour, total_enrolled, total_sent, total_opened, total_clicked, created_at')
|
|
654
|
+
.select('id, site_id, name, type, status, sends_per_hour, total_enrolled, total_sent, total_opened, total_clicked, created_at, period_key')
|
|
653
655
|
.eq('project_id', getProjectId())
|
|
654
656
|
.order('created_at', { ascending: false })
|
|
655
657
|
.limit(limit ?? 25);
|
|
@@ -664,10 +666,16 @@ export function registerCampaignTools(server) {
|
|
|
664
666
|
// counts that cannot exceed total_sent. null = the people-count lookup failed (unknown, not 0).
|
|
665
667
|
const rows = data ?? [];
|
|
666
668
|
const engagement = await countUniqueEngagementByCampaign(db(), getProjectId(), rows.map((row) => row.id));
|
|
667
|
-
const withPeople = rows.map((row) =>
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
669
|
+
const withPeople = await Promise.all(rows.map(async (row) => {
|
|
670
|
+
const periods = await readCampaignPeriods(db(), getProjectId(), row);
|
|
671
|
+
const { period_key: _periodKey, ...legacy } = row;
|
|
672
|
+
return {
|
|
673
|
+
...legacy,
|
|
674
|
+
...campaignPeriodFields(periods, period),
|
|
675
|
+
...(periods?.totals ? { total_sent: periods.totals.sent } : {}),
|
|
676
|
+
unique_opened: periods?.totals?.opened ?? (engagement.ok ? engagement.opened.get(row.id) ?? 0 : null),
|
|
677
|
+
unique_clicked: periods?.totals?.clicked ?? (engagement.ok ? engagement.clicked.get(row.id) ?? 0 : null),
|
|
678
|
+
};
|
|
671
679
|
}));
|
|
672
680
|
return { content: [{ type: 'text', text: JSON.stringify(withPeople, null, 2) }] };
|
|
673
681
|
});
|
|
@@ -3409,8 +3417,8 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3409
3417
|
openWorldHint: false,
|
|
3410
3418
|
},
|
|
3411
3419
|
description: 'Get full details of a single campaign including all steps, current enrollment counts, and settings.',
|
|
3412
|
-
inputSchema: { campaign_id: z.string().uuid() },
|
|
3413
|
-
}, async ({ campaign_id }) => {
|
|
3420
|
+
inputSchema: { campaign_id: z.string().uuid(), period: z.string().min(1).optional().describe("Return results for one enrollment period; totals remain all-time.") },
|
|
3421
|
+
}, async ({ campaign_id, period }) => {
|
|
3414
3422
|
const projectId = getProjectId();
|
|
3415
3423
|
const { data: campaign, error: campErr } = await db()
|
|
3416
3424
|
.from('campaigns')
|
|
@@ -3465,6 +3473,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3465
3473
|
const visibilityError = stepsError ?? enrollError ?? sentError ?? queuedEmailError ?? failedSendError ?? queueError ?? dlqError;
|
|
3466
3474
|
if (visibilityError)
|
|
3467
3475
|
return j({ error: visibilityError.message, code: 'CAMPAIGN_VISIBILITY_QUERY_FAILED' });
|
|
3476
|
+
const periods = await readCampaignPeriods(db(), projectId, campaign);
|
|
3468
3477
|
const outstandingRows = queueRows ?? [];
|
|
3469
3478
|
const pendingOrRetryRows = outstandingRows.filter((row) => row.status !== 'processing');
|
|
3470
3479
|
const retryingCount = outstandingRows.filter((row) => row.status === 'retry').length;
|
|
@@ -3478,7 +3487,9 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3478
3487
|
const unresolvedCount = dlqUnresolvedCount ?? 0;
|
|
3479
3488
|
return j({
|
|
3480
3489
|
...campaign,
|
|
3481
|
-
total_sent: sentCount ?? 0,
|
|
3490
|
+
total_sent: periods?.totals?.sent ?? sentCount ?? 0,
|
|
3491
|
+
...(periods?.totals ? { unique_opened: periods.totals.opened, unique_clicked: periods.totals.clicked } : {}),
|
|
3492
|
+
...campaignPeriodFields(periods, period),
|
|
3482
3493
|
steps: steps ?? [],
|
|
3483
3494
|
active_enrollments: enrollCount ?? 0,
|
|
3484
3495
|
send_incomplete: queuedCount > 0 || failedCount > 0 || outstandingRows.length > 0 || unresolvedCount > 0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.101",
|
|
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",
|