@sendinel/mcp-server 1.0.98 → 1.0.100
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 +8 -2
- package/build/db.d.ts +1 -0
- package/build/lib/action-approvals.js +1 -0
- package/build/lib/v1-client.js +1 -0
- package/build/server.js +1 -1
- package/build/tools/contacts.js +70 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,13 @@ AI-controlled email operations control plane for campaigns, contacts, segments,
|
|
|
12
12
|
|
|
13
13
|
## Tool groups
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
`emit_contact_event` (contacts, write scope) records an event and can enroll
|
|
16
|
+
contacts into active sending campaigns. It requires standard human approval,
|
|
17
|
+
preserves REST outcomes, and redacts failures. See the
|
|
18
|
+
[event-triggered campaign guide](../../docs/event-triggered-campaigns.md) for
|
|
19
|
+
inputs, idempotency, subscription requirements, and conditional step examples.
|
|
20
|
+
|
|
21
|
+
Sendinel currently exposes 35 tool groups and 340 tools (gateway mode exposes 4 meta-tools, sharing how_to with full mode). These counts are checked against the tool registry in CI, so this table matches what ships.
|
|
16
22
|
|
|
17
23
|
| Group | Count | Purpose |
|
|
18
24
|
|-------|-------|---------|
|
|
@@ -28,7 +34,7 @@ Sendinel currently exposes 35 tool groups and 339 tools (gateway mode exposes 4
|
|
|
28
34
|
| `cloud-import` | 6 | Cloud storage import |
|
|
29
35
|
| `compound` | 4 | Higher-level campaign/template/site setup and delivery diagnosis workflows |
|
|
30
36
|
| `connector` | 6 | BYOD connector status, table discovery, schema/view setup, mapping, resync |
|
|
31
|
-
| `contacts` |
|
|
37
|
+
| `contacts` | 39 | Subscriber CRUD, saved views, tags, email log, suppression, merge, timeline |
|
|
32
38
|
| `content` | 14 | Content blocks, gated asset links, blog ingest and auto-publish, sources |
|
|
33
39
|
| `data` | 19 | Platform connectors, imports, API keys, migration jobs, notifications, tenant export |
|
|
34
40
|
| `data-proposals` | 3 | Data-derived campaign proposal generation, listing, approval |
|
package/build/db.d.ts
CHANGED
|
@@ -54,6 +54,7 @@ const GATED_ACTIONS = new Set([
|
|
|
54
54
|
'tenant_export_full', // copies all customer data out of Sendinel
|
|
55
55
|
'enroll_contact', // starts real emails to a contact
|
|
56
56
|
'identify_subscriber', // may enroll a new signup in sending campaigns
|
|
57
|
+
'emit_contact_event', // may enroll a real contact in active sending campaigns
|
|
57
58
|
'enroll_automation', // starts automated messages to a contact
|
|
58
59
|
'enroll_in_escalation_ladder', // starts email/SMS escalation to a contact
|
|
59
60
|
'set_contact_sms_consent', // changes the consent record that allows texting
|
package/build/lib/v1-client.js
CHANGED
|
@@ -28,6 +28,7 @@ export async function callSendinelV1(path, opts = {}) {
|
|
|
28
28
|
Authorization: `Bearer ${apiKey}`,
|
|
29
29
|
'X-Sendinel-Project-Id': getProjectId(),
|
|
30
30
|
'Content-Type': 'application/json',
|
|
31
|
+
...(opts.idempotencyKey ? { 'Idempotency-Key': opts.idempotencyKey } : {}),
|
|
31
32
|
},
|
|
32
33
|
...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
|
|
33
34
|
});
|
package/build/server.js
CHANGED
|
@@ -82,7 +82,7 @@ export const ALL_TOOL_GROUPS = Object.keys(GROUP_REGISTRY);
|
|
|
82
82
|
const WRITE_TOOLS = new Set([
|
|
83
83
|
'recheck_domain_dns',
|
|
84
84
|
'mark_agent_message', 'draft_agent_reply', 'create_agent_mailbox',
|
|
85
|
-
'add_subscriber', 'identify_subscriber', 'import_subscribers', 'update_subscriber', 'bulk_update_subscribers',
|
|
85
|
+
'add_subscriber', 'identify_subscriber', 'emit_contact_event', 'import_subscribers', 'update_subscriber', 'bulk_update_subscribers',
|
|
86
86
|
'create_subscriber_view', 'update_subscriber_view', 'delete_subscriber_view',
|
|
87
87
|
'enrich_subscriber',
|
|
88
88
|
'create_campaign', 'update_campaign', 'delete_campaign', 'add_campaign_step', 'update_campaign_status',
|
package/build/tools/contacts.js
CHANGED
|
@@ -536,6 +536,41 @@ async function exportSubscriberData(projectId, contactId) {
|
|
|
536
536
|
};
|
|
537
537
|
}
|
|
538
538
|
export function registerContactTools(server) {
|
|
539
|
+
server.registerTool('emit_contact_event', {
|
|
540
|
+
title: 'Emit Contact Event',
|
|
541
|
+
description: 'Record a contact event through POST /api/v1/events and enroll matching active triggered campaigns. Can start real sends; requires standard approval. Returns the REST outcome and sendability unchanged. Does not subscribe the contact or update contact properties.',
|
|
542
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
543
|
+
inputSchema: {
|
|
544
|
+
approval_id: z.string().uuid().optional().describe('Call once to request approval, then repeat the same arguments with the approved approval_id.'),
|
|
545
|
+
email: z.string().email(),
|
|
546
|
+
event: z.string().trim().min(1),
|
|
547
|
+
data: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
548
|
+
idempotency_key: z.string().min(1).optional().describe('Sent as Idempotency-Key. Reuse with the same event payload when retrying.'),
|
|
549
|
+
},
|
|
550
|
+
}, withApproval('emit_contact_event', async ({ email, event, data, idempotency_key }) => {
|
|
551
|
+
// Never forward raw transport, database or provider errors to the MCP caller.
|
|
552
|
+
// Only known public codes survive; even a returned message may contain internal text.
|
|
553
|
+
const publicCodes = new Set([
|
|
554
|
+
'contact_limit', 'invalid_consent_site', 'contact_create_failed', 'event_record_failed', 'event_intake_failed',
|
|
555
|
+
'IDEMPOTENCY_UNAVAILABLE', 'IDEMPOTENCY_KEY_REUSED', 'IDEMPOTENCY_IN_FLIGHT',
|
|
556
|
+
]);
|
|
557
|
+
try {
|
|
558
|
+
const result = await callSendinelV1('/api/v1/events', {
|
|
559
|
+
method: 'POST',
|
|
560
|
+
body: { email, event, ...(data !== undefined ? { data } : {}) },
|
|
561
|
+
...(idempotency_key ? { idempotencyKey: idempotency_key } : {}),
|
|
562
|
+
});
|
|
563
|
+
if (result.ok)
|
|
564
|
+
return j(result.data);
|
|
565
|
+
const payload = result.error && typeof result.error === 'object' && 'error' in result.error
|
|
566
|
+
&& result.error.error && typeof result.error.error === 'object' ? result.error.error : result.error;
|
|
567
|
+
const code = payload && typeof payload === 'object' && 'code' in payload ? payload.code : undefined;
|
|
568
|
+
return { ...j({ code: typeof code === 'string' && publicCodes.has(code) ? code : 'event_intake_failed', error: 'Could not emit contact event' }), isError: true };
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
return { ...j({ code: 'event_intake_failed', error: 'Could not emit contact event' }), isError: true };
|
|
572
|
+
}
|
|
573
|
+
}));
|
|
539
574
|
server.registerTool('identify_subscriber', {
|
|
540
575
|
title: 'Identify Subscriber',
|
|
541
576
|
description: 'Identify or update a contact through the v1 identify API. Optional signup/customer consent subscribes new or existing contacts without overriding opt-outs. Customer consent requires a topic; consent requires site_id. Without consent, site_id retains legacy create-only subscription behavior and returns a warning.',
|
|
@@ -1131,21 +1166,27 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1131
1166
|
if (!rows.length)
|
|
1132
1167
|
return j([]);
|
|
1133
1168
|
const companyIds = rows.map((company) => company.id);
|
|
1134
|
-
const { data: subscribers, error: subscriberError } = await db()
|
|
1135
|
-
.from('subscribers')
|
|
1136
|
-
.select('company_id, engagement_score, total_opens, total_clicks, lifecycle_stage, last_engaged_at')
|
|
1137
|
-
.eq('project_id', projectId)
|
|
1138
|
-
.in('company_id', companyIds);
|
|
1139
|
-
if (subscriberError)
|
|
1140
|
-
return txt(`Error: ${subscriberError.message}`);
|
|
1141
1169
|
const grouped = new Map();
|
|
1142
|
-
for (
|
|
1143
|
-
const
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1170
|
+
for (let offset = 0;; offset += 1000) {
|
|
1171
|
+
const { data: subscribers, error: subscriberError } = await db()
|
|
1172
|
+
.from('subscribers')
|
|
1173
|
+
.select('company_id, engagement_score, total_opens, total_clicks, lifecycle_stage, last_engaged_at')
|
|
1174
|
+
.eq('project_id', projectId)
|
|
1175
|
+
.in('company_id', companyIds)
|
|
1176
|
+
.order('id')
|
|
1177
|
+
.range(offset, offset + 999);
|
|
1178
|
+
if (subscriberError)
|
|
1179
|
+
return txt(`Error: ${subscriberError.message}`);
|
|
1180
|
+
for (const subscriber of subscribers ?? []) {
|
|
1181
|
+
const companyId = typeof subscriber.company_id === 'string' ? subscriber.company_id : null;
|
|
1182
|
+
if (!companyId)
|
|
1183
|
+
continue;
|
|
1184
|
+
const bucket = grouped.get(companyId) ?? [];
|
|
1185
|
+
bucket.push(subscriber);
|
|
1186
|
+
grouped.set(companyId, bucket);
|
|
1187
|
+
}
|
|
1188
|
+
if ((subscribers ?? []).length < 1000)
|
|
1189
|
+
break;
|
|
1149
1190
|
}
|
|
1150
1191
|
return j(rows.map((company) => ({
|
|
1151
1192
|
...company,
|
|
@@ -2161,11 +2202,21 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2161
2202
|
return j({ inactive_count: 0, sample: [], message: 'No subscribed contacts for this site.' });
|
|
2162
2203
|
q = q.in('id', ids);
|
|
2163
2204
|
}
|
|
2164
|
-
const
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2205
|
+
const suppressedSet = new Set();
|
|
2206
|
+
for (let offset = 0;; offset += 1000) {
|
|
2207
|
+
const { data: suppressed, error: suppressionError } = await db()
|
|
2208
|
+
.from('suppressions')
|
|
2209
|
+
.select('email')
|
|
2210
|
+
.eq('project_id', projectId)
|
|
2211
|
+
.order('email')
|
|
2212
|
+
.range(offset, offset + 999);
|
|
2213
|
+
if (suppressionError)
|
|
2214
|
+
return txt(`Error: ${suppressionError.message}`);
|
|
2215
|
+
for (const suppression of suppressed ?? [])
|
|
2216
|
+
suppressedSet.add(suppression.email);
|
|
2217
|
+
if ((suppressed ?? []).length < 1000)
|
|
2218
|
+
break;
|
|
2219
|
+
}
|
|
2169
2220
|
const { data: contacts, error } = await q.order('engagement_score', { ascending: true }).limit(sampleLimit);
|
|
2170
2221
|
if (error)
|
|
2171
2222
|
return txt(`Error: ${error.message}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.100",
|
|
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",
|