@sendinel/mcp-server 1.0.192 → 1.0.193

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.
@@ -15,4 +15,17 @@ export declare function storedDomainDns(health?: StoredHealth | null): {
15
15
  };
16
16
  export declare function readLatestStoredDomainHealth(domainId: string): any;
17
17
  export declare function storedDnsReasons(dns: ReturnType<typeof storedDomainDns>): string[];
18
+ export type DnsCheckState = 'not_checked' | 'pass' | 'fail';
19
+ /**
20
+ * Explicit, additive rendering of a stored DNS observation. null never means pass or fail:
21
+ * it is 'not_checked'. dns_note says why this can differ from the domain's `verified` status.
22
+ */
23
+ export declare function dnsCheckSummary(dns: ReturnType<typeof storedDomainDns>): {
24
+ spf: DnsCheckState;
25
+ dkim: DnsCheckState;
26
+ dmarc: DnsCheckState;
27
+ dns_check_status: string;
28
+ checked_at: string;
29
+ dns_note: string | null;
30
+ };
18
31
  export {};
@@ -32,3 +32,20 @@ export function storedDnsReasons(dns) {
32
32
  }
33
33
  return reasons;
34
34
  }
35
+ const dnsState = (value) => value === null ? 'not_checked' : value ? 'pass' : 'fail';
36
+ /**
37
+ * Explicit, additive rendering of a stored DNS observation. null never means pass or fail:
38
+ * it is 'not_checked'. dns_note says why this can differ from the domain's `verified` status.
39
+ */
40
+ export function dnsCheckSummary(dns) {
41
+ const states = { spf: dnsState(dns.spf), dkim: dnsState(dns.dkim), dmarc: dnsState(dns.dmarc) };
42
+ const values = Object.values(states);
43
+ const dns_check_status = values.every(s => s === 'not_checked') ? 'not_checked'
44
+ : values.includes('not_checked') ? 'partially_checked' : 'checked';
45
+ const when = dns.checked_at ? `last DNS check ${dns.checked_at}` : 'DNS never checked (checked_at: never)';
46
+ const basis = 'Domain "verified" (list_domains status) is the provider registration status; SPF/DKIM/DMARC come only from the last stored DNS check';
47
+ const dns_note = dns_check_status === 'checked' && !Object.values(states).includes('fail') ? null
48
+ : dns_check_status === 'not_checked' ? `${basis}. ${when}: not_checked means unknown, not failing. Run refresh_domain_dns.`
49
+ : `${basis}. ${when}; not_checked means unknown, not failing${values.includes('fail') ? '; fail means the stored DNS check found a problem' : ''}.`;
50
+ return { ...states, dns_check_status, checked_at: dns.checked_at ?? 'never', dns_note };
51
+ }
@@ -7,6 +7,7 @@ import { getProjectId } from '../project.js';
7
7
  import { toolError, dbToolError } from '../lib/db-error.js';
8
8
  import { buildVoicePrompt } from '../lib/voice.js';
9
9
  import { getAnthropicModelId } from '../anthropic-model.js';
10
+ import { storedDomainDns, dnsCheckSummary } from '../lib/domain-health.js';
10
11
  import { dkimRemediationText } from '../lib/email-provider.js';
11
12
  const txt = (text) => ({ content: [{ type: 'text', text }] });
12
13
  const j = (data) => txt(JSON.stringify(data, null, 2));
@@ -641,7 +642,7 @@ Return ONLY valid JSON (no markdown fences): { "topics": [{ "title": "...", "rat
641
642
  // 2. Fetch latest domain_health
642
643
  const { data: healthRows, error: healthError } = await db()
643
644
  .from('domain_health')
644
- .select('spf_valid, dkim_valid, dmarc_valid, bounce_rate, complaint_rate, warmup_day, warmup_daily_limit, delivery_rate, total_sent')
645
+ .select('check_date, spf_valid, dkim_valid, dmarc_valid, bounce_rate, complaint_rate, warmup_day, warmup_daily_limit, delivery_rate, total_sent')
645
646
  .eq('site_id', site_id)
646
647
  .order('check_date', { ascending: false })
647
648
  .limit(1);
@@ -693,6 +694,10 @@ Return ONLY valid JSON (no markdown fences): { "topics": [{ "title": "...", "rat
693
694
  issues.push({ type: 'elevated_complaint', severity: 'high', detail: `Complaint rate ${complaintRate}% approaching danger zone` });
694
695
  if (spfFailed || dkimFailed)
695
696
  issues.push({ type: 'dns_invalid', severity: 'high', detail: `Missing DNS: ${spfFailed ? 'SPF' : ''}${spfFailed && dkimFailed ? ' + ' : ''}${dkimFailed ? 'DKIM' : ''}` });
697
+ const storedDns = storedDomainDns(health);
698
+ const dnsUnchecked = [spfValid, dkimValid, dmarcValid].some(v => v === null);
699
+ if (dnsUnchecked)
700
+ issues.push({ type: 'dns_unchecked', severity: 'low', detail: 'SPF/DKIM/DMARC have not been checked in stored domain health (unknown, not failing). Run refresh_domain_dns.' });
696
701
  if (dmarcFailed)
697
702
  issues.push({ type: 'no_dmarc', severity: 'medium', detail: 'DMARC record missing or invalid' });
698
703
  if (unchecked.length)
@@ -806,7 +811,7 @@ Return ONLY valid JSON (no markdown fences): { "topics": [{ "title": "...", "rat
806
811
  complaint_rate: `${complaintRate}%`,
807
812
  open_rate: `${openRate}%`,
808
813
  },
809
- dns: { spf_valid: spfValid, dkim_valid: dkimValid, dmarc_valid: dmarcValid },
814
+ dns: { spf_valid: spfValid, dkim_valid: dkimValid, dmarc_valid: dmarcValid, ...dnsCheckSummary(storedDns), verification_source: storedDns.verification_source },
810
815
  warmup: warmupDay != null ? { day: warmupDay, daily_limit: warmupDailyLimit } : null,
811
816
  new_suppressions_7d: newSuppressions ?? 0,
812
817
  issues,
@@ -9,7 +9,7 @@ import { getProjectId } from '../project.js';
9
9
  import { resolveProjectClient } from '../byod-client.js';
10
10
  import { logAudit } from '../audit.js';
11
11
  import { dbToolError, toolError } from '../lib/db-error.js';
12
- import { storedDomainDns, storedDnsReasons } from '../lib/domain-health.js';
12
+ import { storedDomainDns, storedDnsReasons, dnsCheckSummary } from '../lib/domain-health.js';
13
13
  const txt = (text) => ({ content: [{ type: 'text', text }] });
14
14
  const j = (data) => txt(JSON.stringify(data, null, 2));
15
15
  const percent = (rate) => `${(rate * 100).toFixed(1)}%`;
@@ -597,7 +597,7 @@ export function registerAnalyticsTools(server) {
597
597
  .limit(days);
598
598
  if (error)
599
599
  return toolError(dbToolError(error, 'Unable to load domain health'));
600
- return j((data ?? []).map(({ sites: _sites, ...row }) => ({ ...row, dns: storedDomainDns(row) })));
600
+ return j((data ?? []).map(({ sites: _sites, ...row }) => ({ ...row, dns: storedDomainDns(row), dns_check: dnsCheckSummary(storedDomainDns(row)) })));
601
601
  });
602
602
  server.registerTool('get_engagement_insights', {
603
603
  title: 'Get Engagement Insights',
@@ -772,6 +772,7 @@ export function registerAnalyticsTools(server) {
772
772
  resend_domain_status: null, // Retained for compatibility; no live Resend query is made.
773
773
  checked_at: storedDns.checked_at,
774
774
  dns: dnsResults,
775
+ dns_check: dnsCheckSummary(storedDns),
775
776
  warmup: { day: warmupDay, daily_limit: warmupLimit, completed: warmupDay ? warmupDay >= 43 : null },
776
777
  recent_7d: { total_sent: totalSent, bounced, complained, bounce_rate: `${bounceRate.toFixed(2)}%`, complaint_rate: `${complaintRate.toFixed(3)}%` },
777
778
  health_trend: health.slice(0, 7).map((h) => ({ date: h.check_date, sent: h.total_sent, delivery_rate: h.delivery_rate, bounce_rate: h.bounce_rate })),
@@ -7,7 +7,7 @@ import { assertChannelCanExecute, checkApproval, currentChannelContext, requestA
7
7
  import { callSendinelV1 } from '../lib/v1-client.js';
8
8
  import { getProjectId } from '../project.js';
9
9
  import { dbToolError, toolError } from '../lib/db-error.js';
10
- import { readLatestStoredDomainHealth, storedDomainDns, storedDnsReasons } from '../lib/domain-health.js';
10
+ import { readLatestStoredDomainHealth, storedDomainDns, storedDnsReasons, dnsCheckSummary } from '../lib/domain-health.js';
11
11
  const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
12
12
  function asRecord(value) {
13
13
  return value && typeof value === 'object' ? value : {};
@@ -829,7 +829,7 @@ export function registerDeliveryOpsTools(server) {
829
829
  readiness.ready = readiness.ready === false || auth.includes(false) ? false
830
830
  : readiness.ready === null || auth.includes(null) ? null : true;
831
831
  readiness.reasons.push(...storedDnsReasons(dns));
832
- domains.push({ ...domain, dns, readiness });
832
+ domains.push({ ...domain, dns, readiness, registration_status: domain.status, dns_check: dnsCheckSummary(dns) });
833
833
  }
834
834
  return j({ domains, count: domains.length });
835
835
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.192",
3
+ "version": "1.0.193",
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",