@sendinel/mcp-server 1.0.226 → 1.0.228
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 +15 -4
- package/build/tools/contacts.js +3 -2
- package/build/tools/org.js +8 -3
- package/package.json +1 -1
package/build/tools/analytics.js
CHANGED
|
@@ -742,14 +742,22 @@ export function registerAnalyticsTools(server) {
|
|
|
742
742
|
};
|
|
743
743
|
const warmupDay = latest?.warmup_day ?? null;
|
|
744
744
|
const warmupLimit = latest?.warmup_daily_limit ?? null;
|
|
745
|
-
|
|
746
|
-
const
|
|
745
|
+
// Last 7 UTC days (today and the 6 before). recent_7d and health_trend share these rows (#4840).
|
|
746
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
747
|
+
const todayStart = new Date(new Date().toISOString().slice(0, 10) + 'T00:00:00.000Z').getTime();
|
|
748
|
+
const trendDates = Array.from({ length: 7 }, (_, i) => new Date(todayStart - i * DAY_MS).toISOString().slice(0, 10));
|
|
749
|
+
const sevenDaysAgo = new Date(todayStart - 6 * DAY_MS).toISOString();
|
|
750
|
+
const { data: recentLogs, error: logError } = await db()
|
|
747
751
|
.from('email_log')
|
|
748
|
-
.select("status, bounced_at, opened_at, clicked_at, contact_id, to_email")
|
|
752
|
+
.select("status, bounced_at, opened_at, clicked_at, contact_id, to_email, sent_at")
|
|
749
753
|
.eq('project_id', projectId)
|
|
750
754
|
.eq('site_id', site_id)
|
|
751
755
|
.gte('sent_at', sevenDaysAgo);
|
|
756
|
+
// A failed read is not seven days of zero sends.
|
|
757
|
+
if (logError)
|
|
758
|
+
return toolError(dbToolError(logError, 'Unable to load recent email activity'));
|
|
752
759
|
const logs = recentLogs ?? [];
|
|
760
|
+
const logsByDay = groupMetricRows(logs, l => l.sent_at ? new Date(l.sent_at).toISOString().slice(0, 10) : 'no_send_time');
|
|
753
761
|
const totalSent = countEmailRows(logs).sent;
|
|
754
762
|
const bounced = logs.filter((l) => isBouncedRow(l)).length;
|
|
755
763
|
const complained = logs.filter(isComplainedRow).length;
|
|
@@ -775,7 +783,10 @@ export function registerAnalyticsTools(server) {
|
|
|
775
783
|
dns_check: dnsCheckSummary(storedDns),
|
|
776
784
|
warmup: { day: warmupDay, daily_limit: warmupLimit, completed: warmupDay ? warmupDay >= 43 : null },
|
|
777
785
|
recent_7d: { total_sent: totalSent, bounced, complained, bounce_rate: `${bounceRate.toFixed(2)}%`, complaint_rate: `${complaintRate.toFixed(3)}%` },
|
|
778
|
-
health_trend:
|
|
786
|
+
health_trend: trendDates.map(date => {
|
|
787
|
+
const counts = countEmailRows(logsByDay.get(date) ?? []);
|
|
788
|
+
return { date, sent: counts.sent, delivery_rate: percentRate(counts.delivered, counts.sent, 2), bounce_rate: percentRate(counts.bounced, counts.sent, 2) };
|
|
789
|
+
}),
|
|
779
790
|
risks,
|
|
780
791
|
overall: risks.length === 0 ? 'HEALTHY' : risks.some(r => r.startsWith('HIGH')) ? 'CRITICAL' : 'WARNING',
|
|
781
792
|
});
|
package/build/tools/contacts.js
CHANGED
|
@@ -1609,7 +1609,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1609
1609
|
idempotentHint: true,
|
|
1610
1610
|
openWorldHint: false,
|
|
1611
1611
|
},
|
|
1612
|
-
description: 'See sent emails and their delivery status: the email history for your project, one row per message with recipient, subject, send time, status (sent, delivered, opened, clicked, bounced, complained, failed) and where it came from (source: campaign or transactional). Filter by recipient email, subject words, campaign, site, status, and a sent-between date range (sent_after / sent_before, ISO dates; a date alone covers the whole day, UTC). Newest first. Page past 200 with offset: when a filter or offset is used the answer is { emails, has_more, next_offset }; with no filters it is the plain list.',
|
|
1612
|
+
description: 'See sent emails and their delivery status: the email history for your project, one row per message with recipient, subject, send time, status (sent, delivered, opened, clicked, bounced, complained, failed) and where it came from (source: campaign or transactional). Filter by recipient email, subject words, campaign, site, status, and a sent-between date range (sent_after / sent_before, ISO dates; a date alone covers the whole day, UTC). The date range matches only rows that have a send time, so failed or refused emails without one are left out when you use it. Newest first by creation time. Page past 200 with offset: when a filter or offset is used the answer is { emails, has_more, next_offset }; with no filters it is the plain list.',
|
|
1613
1613
|
inputSchema: {
|
|
1614
1614
|
site_id: z.string().uuid().optional(),
|
|
1615
1615
|
contact_email: z.string().optional().describe('Exact recipient email address.'),
|
|
@@ -1641,7 +1641,8 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1641
1641
|
.from('email_log')
|
|
1642
1642
|
.select('id, to_email, from_email, subject, status, category, sent_at, delivered_at, opened_at, clicked_at, bounced_at, bounce_type, complained_at, error_message, site_id, campaign_id, created_at')
|
|
1643
1643
|
.eq('project_id', getProjectId())
|
|
1644
|
-
|
|
1644
|
+
// created_at is never null, so refused/failed rows (no sent_at) keep their place in time (#4841).
|
|
1645
|
+
.order('created_at', { ascending: false })
|
|
1645
1646
|
.order('id', { ascending: false })
|
|
1646
1647
|
.range(start, start + pageSize);
|
|
1647
1648
|
if (site_id)
|
package/build/tools/org.js
CHANGED
|
@@ -565,8 +565,11 @@ export function registerOrgTools(server) {
|
|
|
565
565
|
inputSchema: {},
|
|
566
566
|
}, async () => {
|
|
567
567
|
const projectId = getProjectId();
|
|
568
|
-
const { data: project } = await db().from('projects').select('plan').eq('id', projectId).maybeSingle();
|
|
568
|
+
const { data: project } = await db().from('projects').select('plan, compute_path').eq('id', projectId).maybeSingle();
|
|
569
569
|
const plan = project?.plan ?? 'free';
|
|
570
|
+
// Mirrors the SQL quota check: a missing path is managed, and free is managed unless it is codex_bridge.
|
|
571
|
+
const computePath = project?.compute_path ?? 'managed';
|
|
572
|
+
const aiNotMetered = (plan === 'free' && computePath !== 'codex_bridge' ? 'managed' : computePath) !== 'managed';
|
|
570
573
|
const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free ?? {};
|
|
571
574
|
const [contacts, sends, drafts, aiTokens, { count: sites }] = await Promise.all([
|
|
572
575
|
readPlanUsage(db(), projectId, "contacts"),
|
|
@@ -575,7 +578,7 @@ export function registerOrgTools(server) {
|
|
|
575
578
|
data: null,
|
|
576
579
|
error: error instanceof Error ? error.message : 'Unable to read draft usage',
|
|
577
580
|
})),
|
|
578
|
-
readAiTokenUsage(db(), projectId),
|
|
581
|
+
aiNotMetered ? Promise.resolve(null) : readAiTokenUsage(db(), projectId),
|
|
579
582
|
db().from('sites').select('id', { count: 'exact', head: true }).eq('project_id', projectId),
|
|
580
583
|
]);
|
|
581
584
|
if (contacts.error || sends.error || contacts.data?.error || sends.data?.error || !contacts.data || !sends.data) {
|
|
@@ -589,7 +592,9 @@ export function registerOrgTools(server) {
|
|
|
589
592
|
ai_drafts: typeof drafts.data?.current === 'number'
|
|
590
593
|
? { used: drafts.data.current, limit: limits.ai_drafts ?? null }
|
|
591
594
|
: { used: null, limit: limits.ai_drafts ?? null, reason: 'Unable to read draft usage' },
|
|
592
|
-
ai_tokens:
|
|
595
|
+
ai_tokens: aiNotMetered
|
|
596
|
+
? { used: null, limit: null, reason: "Not metered: this project's AI runs on its own compute path, so there is no platform token allowance." }
|
|
597
|
+
: aiTokens ?? { used: null, limit: null, reason: 'Unable to read AI token usage' },
|
|
593
598
|
log_retention_days: limits.log_retention_days ?? null,
|
|
594
599
|
});
|
|
595
600
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.228",
|
|
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",
|