@sendinel/mcp-server 1.0.66 → 1.0.67
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 +2 -2
- package/build/db.d.ts +2 -0
- package/build/db.js +1 -0
- package/build/lib/action-approvals.d.ts +1 -0
- package/build/lib/action-approvals.js +5 -2
- package/build/lib/outbound-digest.d.ts +57 -0
- package/build/lib/outbound-digest.js +75 -0
- package/build/tools/org.js +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ AI-controlled email operations control plane for campaigns, contacts, segments,
|
|
|
12
12
|
|
|
13
13
|
## Tool groups
|
|
14
14
|
|
|
15
|
-
Sendinel currently exposes 34 tool groups and
|
|
15
|
+
Sendinel currently exposes 34 tool groups and 329 tools (gateway mode adds 3 meta-tools). These counts are checked against the tool registry in CI, so this table matches what ships.
|
|
16
16
|
|
|
17
17
|
| Group | Count | Purpose |
|
|
18
18
|
|-------|-------|---------|
|
|
@@ -38,7 +38,7 @@ Sendinel currently exposes 34 tool groups and 328 tools (gateway mode adds 3 met
|
|
|
38
38
|
| `landing-pages` | 5 | Landing page CRUD |
|
|
39
39
|
| `lists` | 7 | Static contact lists and membership management |
|
|
40
40
|
| `newsletters` | 12 | Newsletter publications, issues, subscribers, scheduling, sending, stats |
|
|
41
|
-
| `org` |
|
|
41
|
+
| `org` | 24 | Team members, plan usage/limits, audit log, model registry, white-label, support tickets, outbound digest, provider switching |
|
|
42
42
|
| `segments` | 6 | Audience builder: create, edit, natural-language generation, preview |
|
|
43
43
|
| `short-links` | 3 | Short links, click stats, QR codes |
|
|
44
44
|
| `signatures` | 4 | Sending signature blocks |
|
package/build/db.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ export declare function runWithClient<T>(client: any, fn: () => Promise<T>, requ
|
|
|
40
40
|
oauthTokenId?: string | null;
|
|
41
41
|
authKind?: string | null;
|
|
42
42
|
oauthClientId?: string | null;
|
|
43
|
+
agentLabel?: string | null;
|
|
43
44
|
keyId?: string | null;
|
|
44
45
|
workspaceId?: string | null;
|
|
45
46
|
v1Caller?: V1Caller;
|
|
@@ -59,6 +60,7 @@ export declare function requestContext(): {
|
|
|
59
60
|
oauthTokenId?: string | null;
|
|
60
61
|
authKind?: string | null;
|
|
61
62
|
oauthClientId?: string | null;
|
|
63
|
+
agentLabel?: string | null;
|
|
62
64
|
keyId?: string | null;
|
|
63
65
|
workspaceId?: string | null;
|
|
64
66
|
v1Caller?: V1Caller;
|
package/build/db.js
CHANGED
|
@@ -48,6 +48,7 @@ export interface ApprovalResult {
|
|
|
48
48
|
approve_url: string;
|
|
49
49
|
message: string;
|
|
50
50
|
}
|
|
51
|
+
export declare function approvalPageUrl(approvalId: string): string;
|
|
51
52
|
/**
|
|
52
53
|
* Request human approval for an action. Creates a pending approval token
|
|
53
54
|
* and returns instructions for the agent to wait.
|
|
@@ -282,7 +282,7 @@ export function buildApprovalSummary(action, params, display = {}) {
|
|
|
282
282
|
const payload = JSON.stringify(normalizeApprovalParams(params));
|
|
283
283
|
return `Execute ${action} with: ${payload.length > 400 ? `${payload.slice(0, 399)}…` : payload}`;
|
|
284
284
|
}
|
|
285
|
-
function approvalPageUrl(approvalId) {
|
|
285
|
+
export function approvalPageUrl(approvalId) {
|
|
286
286
|
const base = (process.env.NEXT_PUBLIC_APP_URL ?? process.env.SENDINEL_APP_URL ?? process.env.APP_URL ?? 'https://sendinel.ai').replace(/\/$/, '');
|
|
287
287
|
return `${base}/dashboard/approvals/${approvalId}`;
|
|
288
288
|
}
|
|
@@ -310,11 +310,14 @@ export async function requestApproval(action, params, apiKeyId, channelContext,
|
|
|
310
310
|
const summary = buildApprovalSummary(action, params, display);
|
|
311
311
|
const expiresAt = new Date(Date.now() + 30 * 60 * 1000).toISOString();
|
|
312
312
|
const { channel, verified } = channelContext ?? currentChannelContext();
|
|
313
|
+
const actor = requestContext();
|
|
313
314
|
const { data, error } = await db()
|
|
314
315
|
.from('action_approvals')
|
|
315
316
|
.insert({
|
|
316
317
|
project_id: getProjectId(),
|
|
317
|
-
api_key_id: apiKeyId ?? null,
|
|
318
|
+
api_key_id: apiKeyId ?? actor?.keyId ?? null,
|
|
319
|
+
oauth_token_id: actor?.oauthTokenId ?? null,
|
|
320
|
+
agent_label: actor?.agentLabel ?? null,
|
|
318
321
|
action,
|
|
319
322
|
tier,
|
|
320
323
|
params,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
type AgentKind = 'worker' | 'connected_agent' | 'user' | 'system';
|
|
2
|
+
export declare function digestAgent(label: string | null): {
|
|
3
|
+
agent_kind: AgentKind | null;
|
|
4
|
+
agent_id: string | null;
|
|
5
|
+
agent_label: string | null;
|
|
6
|
+
};
|
|
7
|
+
type Counts = {
|
|
8
|
+
sent: number;
|
|
9
|
+
delivered: number;
|
|
10
|
+
bounced: number;
|
|
11
|
+
complained: number;
|
|
12
|
+
opened: number;
|
|
13
|
+
};
|
|
14
|
+
export type OutboundItem = ReturnType<typeof digestAgent> & {
|
|
15
|
+
project_id: string;
|
|
16
|
+
item: {
|
|
17
|
+
type: 'email' | 'broadcast';
|
|
18
|
+
id: string;
|
|
19
|
+
summary: string;
|
|
20
|
+
platform: null;
|
|
21
|
+
preview_url: string | null;
|
|
22
|
+
};
|
|
23
|
+
audience_count: number | null;
|
|
24
|
+
counts: Counts | null;
|
|
25
|
+
is_test: boolean;
|
|
26
|
+
approval_id: string | null;
|
|
27
|
+
approve_url: string | null;
|
|
28
|
+
outcome: 'pending_approval' | 'approved' | 'declined' | 'expired' | 'scheduled' | 'sent' | 'failed';
|
|
29
|
+
approved_by: string | null;
|
|
30
|
+
approved_at: string | null;
|
|
31
|
+
occurred_at: string;
|
|
32
|
+
provider_ref: string | null;
|
|
33
|
+
};
|
|
34
|
+
type DigestDb = {
|
|
35
|
+
schema(name: 'email'): {
|
|
36
|
+
rpc(name: string, args: {
|
|
37
|
+
p_from: string;
|
|
38
|
+
p_to: string;
|
|
39
|
+
p_project_id: string | null;
|
|
40
|
+
}): PromiseLike<{
|
|
41
|
+
data: unknown;
|
|
42
|
+
error: unknown;
|
|
43
|
+
}>;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
/** Sources return scalar JSON arrays: no recipient reads and no PostgREST row cap. */
|
|
47
|
+
export declare function getOutboundDigest(sb: DigestDb, from: string, to: string, projectId?: string): Promise<{
|
|
48
|
+
product: 'sendinel';
|
|
49
|
+
range: {
|
|
50
|
+
from: string;
|
|
51
|
+
to: string;
|
|
52
|
+
};
|
|
53
|
+
status: string;
|
|
54
|
+
unknown_sources: string[];
|
|
55
|
+
items: OutboundItem[];
|
|
56
|
+
}>;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { approvalPageUrl } from './action-approvals.js';
|
|
2
|
+
export function digestAgent(label) {
|
|
3
|
+
const match = label?.match(/^(worker|connected_agent|user|system):([^\s:]+)$/);
|
|
4
|
+
return {
|
|
5
|
+
agent_kind: match ? match[1] : null,
|
|
6
|
+
agent_id: match ? match[2] : null,
|
|
7
|
+
agent_label: label,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function approvalFields(approval) {
|
|
11
|
+
const url = approval ? approvalPageUrl(approval.id) : null;
|
|
12
|
+
return {
|
|
13
|
+
...digestAgent(approval?.agent_label ?? null),
|
|
14
|
+
approval_id: approval?.id ?? null, approve_url: url,
|
|
15
|
+
approved_by: approval?.decided_by ?? null, approved_at: approval?.decided_at ?? null,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Sources return scalar JSON arrays: no recipient reads and no PostgREST row cap. */
|
|
19
|
+
export async function getOutboundDigest(sb, from, to, projectId) {
|
|
20
|
+
const unknownSources = [];
|
|
21
|
+
const args = { p_from: from, p_to: to, p_project_id: projectId ?? null };
|
|
22
|
+
async function source(name, rpc) {
|
|
23
|
+
try {
|
|
24
|
+
const result = await sb.schema('email').rpc(rpc, args);
|
|
25
|
+
if (result.error || !Array.isArray(result.data))
|
|
26
|
+
throw new Error('Source unavailable');
|
|
27
|
+
return result.data;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
unknownSources.push(name);
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Separate reads preserve the readable source if the other fails, including during rollout.
|
|
35
|
+
const approvals = await source('action_approvals', 'outbound_digest_approvals');
|
|
36
|
+
const sends = await source('email_log', 'outbound_digest_sends');
|
|
37
|
+
const items = sends.map(send => {
|
|
38
|
+
const fields = approvalFields(send.approval);
|
|
39
|
+
const broadcast = send.campaign_type === 'broadcast';
|
|
40
|
+
return {
|
|
41
|
+
project_id: send.project_id, ...fields,
|
|
42
|
+
item: {
|
|
43
|
+
type: broadcast ? 'broadcast' : 'email',
|
|
44
|
+
// Standalone logs without a campaign/step are one unknown group per category, never per recipient.
|
|
45
|
+
id: (broadcast ? send.campaign_id : send.step_id) ?? 'unknown',
|
|
46
|
+
summary: send.subject ?? send.campaign_name ?? 'unknown', platform: null, preview_url: fields.approve_url,
|
|
47
|
+
},
|
|
48
|
+
audience_count: send.audience_count, counts: send.counts, is_test: send.category === 'test',
|
|
49
|
+
outcome: send.failed ? 'failed' : 'sent', occurred_at: send.occurred_at, provider_ref: send.provider_ref,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
const sentApprovals = new Set(sends.flatMap(send => send.approval ? [send.approval.id] : []));
|
|
53
|
+
for (const approval of approvals) {
|
|
54
|
+
if (sentApprovals.has(approval.id))
|
|
55
|
+
continue;
|
|
56
|
+
const fields = approvalFields(approval);
|
|
57
|
+
const outcome = approval.status === 'pending'
|
|
58
|
+
? (Date.parse(approval.expires_at) <= Date.parse(to) ? 'expired' : 'pending_approval')
|
|
59
|
+
: approval.status === 'rejected' ? 'declined'
|
|
60
|
+
: approval.status === 'expired' ? 'expired' : 'approved';
|
|
61
|
+
items.push({
|
|
62
|
+
project_id: approval.project_id, ...fields,
|
|
63
|
+
item: { type: approval.action.includes('broadcast') ? 'broadcast' : 'email', id: approval.id,
|
|
64
|
+
summary: approval.summary, platform: null, preview_url: fields.approve_url },
|
|
65
|
+
audience_count: null, counts: null, is_test: approval.action === 'send_test_email',
|
|
66
|
+
outcome, occurred_at: approval.occurred_at, provider_ref: null,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
items.sort((a, b) => a.occurred_at.localeCompare(b.occurred_at) || a.project_id.localeCompare(b.project_id) || a.item.id.localeCompare(b.item.id));
|
|
70
|
+
return {
|
|
71
|
+
product: 'sendinel', range: { from, to },
|
|
72
|
+
status: unknownSources.length === 0 ? 'complete' : unknownSources.length === 2 ? 'unknown' : 'partial',
|
|
73
|
+
unknown_sources: unknownSources, items,
|
|
74
|
+
};
|
|
75
|
+
}
|
package/build/tools/org.js
CHANGED
|
@@ -902,6 +902,23 @@ export function registerOrgTools(server) {
|
|
|
902
902
|
});
|
|
903
903
|
return result.ok ? j(result.data) : j({ error: result.error });
|
|
904
904
|
});
|
|
905
|
+
server.registerTool('get_outbound_digest', {
|
|
906
|
+
title: 'Get Outbound Digest',
|
|
907
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
908
|
+
description: 'Daily fleet outbound digest of approvals and SQL-aggregated sends, including separate test sends. Platform-admin only; tenant credentials receive Not available. Unknown attribution stays null; failed sources are explicit.',
|
|
909
|
+
inputSchema: {
|
|
910
|
+
since: z.iso.datetime({ offset: true }).optional().describe('Start of the window; defaults to 24 hours ago.'),
|
|
911
|
+
project_id: z.string().uuid().optional(),
|
|
912
|
+
},
|
|
913
|
+
}, async ({ since, project_id }) => {
|
|
914
|
+
const query = {};
|
|
915
|
+
if (since)
|
|
916
|
+
query.since = since;
|
|
917
|
+
if (project_id)
|
|
918
|
+
query.project_id = project_id;
|
|
919
|
+
const result = await callSendinelV1('/api/v1/outbound-digest', { method: 'GET', query });
|
|
920
|
+
return result.ok ? j(result.data) : j({ error: result.error });
|
|
921
|
+
});
|
|
905
922
|
server.registerTool('start_provider_switch', {
|
|
906
923
|
title: 'Start Provider Switch',
|
|
907
924
|
annotations: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.67",
|
|
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",
|