@sendinel/mcp-server 1.0.191 → 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.
- package/README.md +3 -1
- package/build/lib/action-approvals.d.ts +2 -0
- package/build/lib/action-approvals.js +1 -1
- package/build/lib/approval-tool-decisions.d.ts +2 -0
- package/build/lib/approval-tool-decisions.js +2 -2
- package/build/lib/domain-health.d.ts +13 -0
- package/build/lib/domain-health.js +17 -0
- package/build/tools/advisor.js +7 -2
- package/build/tools/analytics.js +3 -2
- package/build/tools/approvals.js +2 -2
- package/build/tools/delivery-ops.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,7 +59,9 @@ Sendinel currently exposes 35 tool groups and 349 tools (gateway mode exposes 4
|
|
|
59
59
|
| `webhooks` | 5 | Webhook subscription management |
|
|
60
60
|
|
|
61
61
|
`cancel_approval({approval_id})` rejects an unexpired, unused pending/approved
|
|
62
|
-
request in the current project for any write-scoped credential.
|
|
62
|
+
request in the current project for any write-scoped credential. It returns
|
|
63
|
+
`status: "rejected"` (kept for compatibility) plus `outcome: "cancelled"` and
|
|
64
|
+
`cancelled: true`.
|
|
63
65
|
`approve_approval({approval_id})` requires the person's own live OAuth connection
|
|
64
66
|
(`connection_role: NULL`) and its own pending request in that project. Tester,
|
|
65
67
|
agent, API key, service-token and scoped-JWT credentials cannot approve. Sends
|
|
@@ -100,6 +100,8 @@ export declare function checkApproval(approvalId: string, options?: {
|
|
|
100
100
|
}): Promise<Omit<Awaited<ReturnType<typeof checkApprovalState>>, 'decision_channel'> & {
|
|
101
101
|
decision_channel?: string;
|
|
102
102
|
decision_evidence?: DecisionEvidence;
|
|
103
|
+
outcome?: 'cancelled';
|
|
104
|
+
cancelled?: true;
|
|
103
105
|
}>;
|
|
104
106
|
declare function checkApprovalState(approvalId: string, options?: {
|
|
105
107
|
consume?: boolean;
|
|
@@ -469,7 +469,7 @@ export async function checkApproval(approvalId, options = {}) {
|
|
|
469
469
|
.eq('action', 'approval.cancelled').limit(1);
|
|
470
470
|
const cancel = data?.[0];
|
|
471
471
|
if (!error && cancel?.actor_id && typeof cancel.details?.channel === 'string') {
|
|
472
|
-
return { ...result, decided_by: cancel.actor_id, decision_channel: cancel.details.channel };
|
|
472
|
+
return { ...result, decided_by: cancel.actor_id, decision_channel: cancel.details.channel, outcome: 'cancelled', cancelled: true };
|
|
473
473
|
}
|
|
474
474
|
}
|
|
475
475
|
catch { /* A failed audit read cannot grant authority or change the status. */ }
|
|
@@ -4,6 +4,8 @@ export declare function cancelApproval(approvalId: string, transportClientName?:
|
|
|
4
4
|
} | {
|
|
5
5
|
approval_id: string;
|
|
6
6
|
status: 'rejected';
|
|
7
|
+
outcome: 'cancelled';
|
|
8
|
+
cancelled: true;
|
|
7
9
|
}>;
|
|
8
10
|
export declare function approveApproval(approvalId: string, transportClientName?: string): Promise<{
|
|
9
11
|
code: string;
|
|
@@ -53,12 +53,12 @@ export async function cancelApproval(approvalId, transportClientName) {
|
|
|
53
53
|
project_id: projectId, actor_type: actor.actorType ?? actor.authKind ?? 'api_key',
|
|
54
54
|
actor_id: actor.actorId ?? actor.keyId ?? actor.oauthTokenId ?? null,
|
|
55
55
|
action: 'approval.cancelled', resource_type: 'approval', resource_id: approvalId,
|
|
56
|
-
details: { status: 'rejected', channel, client_name: name, auth_kind: actor.authKind ?? null,
|
|
56
|
+
details: { status: 'rejected', outcome: 'cancelled', channel, client_name: name, auth_kind: actor.authKind ?? null,
|
|
57
57
|
oauth_token_id: actor.oauthTokenId ?? null, api_key_id: actor.keyId ?? null },
|
|
58
58
|
});
|
|
59
59
|
if (auditError)
|
|
60
60
|
return refuse('approval_audit_failed', 'Approval cancelled, but audit failed. Check its status before retrying.');
|
|
61
|
-
return { approval_id: approvalId, status: 'rejected' };
|
|
61
|
+
return { approval_id: approvalId, status: 'rejected', outcome: 'cancelled', cancelled: true };
|
|
62
62
|
}
|
|
63
63
|
catch {
|
|
64
64
|
return refuse('approval_failed', 'Cancellation failed. Check approval status before retrying.');
|
|
@@ -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
|
+
}
|
package/build/tools/advisor.js
CHANGED
|
@@ -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,
|
package/build/tools/analytics.js
CHANGED
|
@@ -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 })),
|
package/build/tools/approvals.js
CHANGED
|
@@ -23,7 +23,7 @@ export function registerApprovalTools(server, options) {
|
|
|
23
23
|
});
|
|
24
24
|
server.registerTool('cancel_approval', {
|
|
25
25
|
title: 'Cancel Approval',
|
|
26
|
-
description: 'Cancel an unexpired, unused pending or approved action approval in this project. Read-scoped OAuth connections can cancel only their own token’s requests; write/admin credentials can cancel any eligible request in this project.
|
|
26
|
+
description: 'Cancel an unexpired, unused pending or approved action approval in this project. Read-scoped OAuth connections can cancel only their own token’s requests; write/admin credentials can cancel any eligible request in this project. Marks it cancelled (stored and reported as status `rejected` for backward compatibility; `outcome: "cancelled"` and `cancelled: true` distinguish a withdrawal from a person’s rejection) and records who cancelled and the request channel in the audit; check_approval returns that cancellation attribution. Cannot approve or undo cancellation.',
|
|
27
27
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
28
28
|
inputSchema: { approval_id: z.string().uuid() },
|
|
29
29
|
}, async ({ approval_id }) => {
|
|
@@ -59,7 +59,7 @@ export function registerApprovalTools(server, options) {
|
|
|
59
59
|
idempotentHint: true,
|
|
60
60
|
openWorldHint: false,
|
|
61
61
|
},
|
|
62
|
-
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
|
|
62
|
+
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; a cancelled approval keeps status `rejected` and adds `outcome: "cancelled"` and `cancelled: true`.',
|
|
63
63
|
inputSchema: {
|
|
64
64
|
approval_id: z.string().uuid().describe('The approval ID returned by the original tool call'),
|
|
65
65
|
},
|
|
@@ -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.
|
|
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",
|