@sendinel/mcp-server 1.0.0
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 +113 -0
- package/build/anthropic-model.d.ts +5 -0
- package/build/anthropic-model.js +6 -0
- package/build/audit.d.ts +21 -0
- package/build/audit.js +38 -0
- package/build/auth.d.ts +10 -0
- package/build/auth.js +57 -0
- package/build/byod-client.d.ts +30 -0
- package/build/byod-client.js +95 -0
- package/build/crypto.d.ts +2 -0
- package/build/crypto.js +27 -0
- package/build/db.d.ts +38 -0
- package/build/db.js +70 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +97 -0
- package/build/lib/action-approvals.d.ts +30 -0
- package/build/lib/action-approvals.js +154 -0
- package/build/lib/schedule.d.ts +36 -0
- package/build/lib/schedule.js +63 -0
- package/build/lib/social-engine/archetypes.d.ts +27 -0
- package/build/lib/social-engine/archetypes.js +160 -0
- package/build/lib/social-engine/index.d.ts +5 -0
- package/build/lib/social-engine/index.js +6 -0
- package/build/lib/social-engine/parse.d.ts +7 -0
- package/build/lib/social-engine/parse.js +83 -0
- package/build/lib/social-engine/platform-constraints.d.ts +27 -0
- package/build/lib/social-engine/platform-constraints.js +70 -0
- package/build/lib/social-engine/prompt-builder.d.ts +13 -0
- package/build/lib/social-engine/prompt-builder.js +80 -0
- package/build/lib/social-engine/types.d.ts +60 -0
- package/build/lib/social-engine/types.js +19 -0
- package/build/lib/url-validation.d.ts +3 -0
- package/build/lib/url-validation.js +51 -0
- package/build/lib/voice.d.ts +32 -0
- package/build/lib/voice.js +140 -0
- package/build/lib/webhook-events.d.ts +15 -0
- package/build/lib/webhook-events.js +120 -0
- package/build/plan-limits.d.ts +8 -0
- package/build/plan-limits.js +9 -0
- package/build/project.d.ts +1 -0
- package/build/project.js +9 -0
- package/build/server.d.ts +18 -0
- package/build/server.js +235 -0
- package/build/tools/ab-testing.d.ts +2 -0
- package/build/tools/ab-testing.js +204 -0
- package/build/tools/advisor.d.ts +23 -0
- package/build/tools/advisor.js +762 -0
- package/build/tools/analytics.d.ts +33 -0
- package/build/tools/analytics.js +1105 -0
- package/build/tools/approvals.d.ts +2 -0
- package/build/tools/approvals.js +32 -0
- package/build/tools/automations.d.ts +2 -0
- package/build/tools/automations.js +344 -0
- package/build/tools/campaigns.d.ts +2 -0
- package/build/tools/campaigns.js +1335 -0
- package/build/tools/compound.d.ts +2 -0
- package/build/tools/compound.js +312 -0
- package/build/tools/contacts.d.ts +2 -0
- package/build/tools/contacts.js +1483 -0
- package/build/tools/content.d.ts +2 -0
- package/build/tools/content.js +68 -0
- package/build/tools/data-proposals.d.ts +2 -0
- package/build/tools/data-proposals.js +155 -0
- package/build/tools/data.d.ts +2 -0
- package/build/tools/data.js +707 -0
- package/build/tools/delivery-ops.d.ts +2 -0
- package/build/tools/delivery-ops.js +387 -0
- package/build/tools/drafts.d.ts +2 -0
- package/build/tools/drafts.js +204 -0
- package/build/tools/forms.d.ts +2 -0
- package/build/tools/forms.js +46 -0
- package/build/tools/gdpr.d.ts +2 -0
- package/build/tools/gdpr.js +61 -0
- package/build/tools/org.d.ts +2 -0
- package/build/tools/org.js +71 -0
- package/build/tools/segments.d.ts +2 -0
- package/build/tools/segments.js +384 -0
- package/build/tools/sites.d.ts +2 -0
- package/build/tools/sites.js +182 -0
- package/build/tools/sms.d.ts +2 -0
- package/build/tools/sms.js +489 -0
- package/build/tools/social-posts.d.ts +2 -0
- package/build/tools/social-posts.js +380 -0
- package/build/tools/templates.d.ts +2 -0
- package/build/tools/templates.js +282 -0
- package/build/tools/warmup.d.ts +2 -0
- package/build/tools/warmup.js +57 -0
- package/build/tools/webhooks.d.ts +2 -0
- package/build/tools/webhooks.js +127 -0
- package/package.json +63 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
3
|
+
import { db } from '../db.js';
|
|
4
|
+
import { getProjectId } from '../project.js';
|
|
5
|
+
import { buildVoicePrompt } from '../lib/voice.js';
|
|
6
|
+
import { getAnthropicFastModelId } from '../anthropic-model.js';
|
|
7
|
+
const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
8
|
+
function getAnthropicClient() {
|
|
9
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
10
|
+
if (!apiKey)
|
|
11
|
+
throw new Error('ANTHROPIC_API_KEY not configured');
|
|
12
|
+
return new Anthropic({ apiKey });
|
|
13
|
+
}
|
|
14
|
+
function extractJsonArray(raw) {
|
|
15
|
+
const match = raw.match(/\[[\s\S]*\]/);
|
|
16
|
+
if (!match)
|
|
17
|
+
throw new Error('No JSON array in response');
|
|
18
|
+
return JSON.parse(match[0]);
|
|
19
|
+
}
|
|
20
|
+
function extractJsonObject(raw) {
|
|
21
|
+
const match = raw.match(/\{[\s\S]*\}/);
|
|
22
|
+
if (!match)
|
|
23
|
+
throw new Error('No JSON object in response');
|
|
24
|
+
return JSON.parse(match[0]);
|
|
25
|
+
}
|
|
26
|
+
export function registerCompoundTools(server) {
|
|
27
|
+
server.registerTool('setup_campaign_from_brief', {
|
|
28
|
+
description: 'Create a complete campaign from a brief: AI-generate steps, create a template, create the campaign, and add all campaign steps.',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
site_id: z.string().uuid(),
|
|
31
|
+
campaign_name: z.string().min(1),
|
|
32
|
+
brief: z.string().min(10),
|
|
33
|
+
num_steps: z.number().int().min(1).max(5).default(3),
|
|
34
|
+
step_delay_hours: z.number().int().min(1).default(72),
|
|
35
|
+
campaign_type: z.enum(['sequence', 'broadcast', 'triggered']).default('sequence'),
|
|
36
|
+
},
|
|
37
|
+
}, async ({ site_id, campaign_name, brief, num_steps, step_delay_hours, campaign_type }) => {
|
|
38
|
+
const projectId = getProjectId();
|
|
39
|
+
const sb = db();
|
|
40
|
+
const { data: site } = await sb.from('sites')
|
|
41
|
+
.select('name, brand_voice')
|
|
42
|
+
.eq('id', site_id)
|
|
43
|
+
.eq('project_id', projectId)
|
|
44
|
+
.maybeSingle();
|
|
45
|
+
const voiceContext = buildVoicePrompt(site?.brand_voice);
|
|
46
|
+
let steps;
|
|
47
|
+
try {
|
|
48
|
+
const msg = await getAnthropicClient().messages.create({
|
|
49
|
+
model: getAnthropicFastModelId(),
|
|
50
|
+
max_tokens: 4096,
|
|
51
|
+
messages: [{
|
|
52
|
+
role: 'user',
|
|
53
|
+
content: `You are writing a ${num_steps}-step email campaign for "${site?.name ?? 'our product'}".
|
|
54
|
+
|
|
55
|
+
CAMPAIGN BRIEF: ${brief}
|
|
56
|
+
|
|
57
|
+
BRAND VOICE: ${voiceContext}
|
|
58
|
+
|
|
59
|
+
Generate exactly ${num_steps} emails. For each email, provide:
|
|
60
|
+
- subject
|
|
61
|
+
- body_html
|
|
62
|
+
- body_text
|
|
63
|
+
- delay_hours_from_previous (0 for the first email, ${step_delay_hours} for subsequent ones)
|
|
64
|
+
|
|
65
|
+
Respond with a JSON array.`,
|
|
66
|
+
}],
|
|
67
|
+
});
|
|
68
|
+
const raw = msg.content[0].text;
|
|
69
|
+
steps = extractJsonArray(raw);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return j({ error: `AI generation failed: ${error instanceof Error ? error.message : String(error)}`, code: 'GENERATION_FAILED' });
|
|
73
|
+
}
|
|
74
|
+
const { data: template, error: templateErr } = await sb.from('campaign_templates')
|
|
75
|
+
.insert({
|
|
76
|
+
project_id: projectId,
|
|
77
|
+
name: `${campaign_name} - Template`,
|
|
78
|
+
brief_template: brief,
|
|
79
|
+
subject_hint: steps[0]?.subject ?? '',
|
|
80
|
+
body_html: steps[0]?.body_html ?? null,
|
|
81
|
+
category: 'campaign',
|
|
82
|
+
description: `Base template generated for campaign "${campaign_name}"`,
|
|
83
|
+
})
|
|
84
|
+
.select('id')
|
|
85
|
+
.single();
|
|
86
|
+
if (templateErr)
|
|
87
|
+
return j({ error: `Template creation failed: ${templateErr.message}` });
|
|
88
|
+
const { data: campaign, error: campaignErr } = await sb.from('campaigns')
|
|
89
|
+
.insert({ name: campaign_name, site_id, project_id: projectId, type: campaign_type, status: 'draft' })
|
|
90
|
+
.select('id, name, status')
|
|
91
|
+
.single();
|
|
92
|
+
if (campaignErr)
|
|
93
|
+
return j({ error: `Campaign creation failed: ${campaignErr.message}` });
|
|
94
|
+
const createdSteps = [];
|
|
95
|
+
for (let i = 0; i < steps.length; i++) {
|
|
96
|
+
const step = steps[i];
|
|
97
|
+
const { data, error } = await sb.from('campaign_steps')
|
|
98
|
+
.insert({
|
|
99
|
+
campaign_id: campaign.id,
|
|
100
|
+
step_order: i,
|
|
101
|
+
subject: step.subject,
|
|
102
|
+
body_html: step.body_html,
|
|
103
|
+
body_text: step.body_text,
|
|
104
|
+
delay_days: 0,
|
|
105
|
+
delay_hours: step.delay_hours_from_previous,
|
|
106
|
+
template_id: String(template.id),
|
|
107
|
+
})
|
|
108
|
+
.select('id, step_order, subject')
|
|
109
|
+
.single();
|
|
110
|
+
if (error)
|
|
111
|
+
return j({ error: `Step ${i + 1} creation failed: ${error.message}`, campaign_id: campaign.id });
|
|
112
|
+
createdSteps.push(data);
|
|
113
|
+
}
|
|
114
|
+
return j({
|
|
115
|
+
ok: true,
|
|
116
|
+
campaign,
|
|
117
|
+
template_id: template.id,
|
|
118
|
+
steps: createdSteps,
|
|
119
|
+
summary: `Created campaign "${campaign_name}" with ${createdSteps.length} steps. Campaign is in draft.`,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
server.registerTool('create_template_from_brief', {
|
|
123
|
+
description: 'AI-generate a polished email template from a brief, using the site brand voice.',
|
|
124
|
+
inputSchema: {
|
|
125
|
+
site_id: z.string().uuid(),
|
|
126
|
+
name: z.string().min(1),
|
|
127
|
+
brief: z.string().min(10),
|
|
128
|
+
category: z.string().optional(),
|
|
129
|
+
},
|
|
130
|
+
}, async ({ site_id, name, brief, category }) => {
|
|
131
|
+
const projectId = getProjectId();
|
|
132
|
+
const { data: site } = await db().from('sites')
|
|
133
|
+
.select('name, brand_voice')
|
|
134
|
+
.eq('id', site_id)
|
|
135
|
+
.eq('project_id', projectId)
|
|
136
|
+
.maybeSingle();
|
|
137
|
+
let generated;
|
|
138
|
+
try {
|
|
139
|
+
const msg = await getAnthropicClient().messages.create({
|
|
140
|
+
model: getAnthropicFastModelId(),
|
|
141
|
+
max_tokens: 2048,
|
|
142
|
+
messages: [{
|
|
143
|
+
role: 'user',
|
|
144
|
+
content: `Write an email for "${site?.name ?? 'our product'}".
|
|
145
|
+
BRIEF: ${brief}
|
|
146
|
+
BRAND VOICE: ${buildVoicePrompt(site?.brand_voice)}
|
|
147
|
+
Respond with JSON: { "subject": "...", "body_html": "...", "body_text": "..." }`,
|
|
148
|
+
}],
|
|
149
|
+
});
|
|
150
|
+
const raw = msg.content[0].text;
|
|
151
|
+
generated = extractJsonObject(raw);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
return j({ error: `Generation failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
155
|
+
}
|
|
156
|
+
const { data: template, error } = await db().from('campaign_templates')
|
|
157
|
+
.insert({
|
|
158
|
+
project_id: projectId,
|
|
159
|
+
name,
|
|
160
|
+
brief_template: brief,
|
|
161
|
+
subject_hint: generated.subject,
|
|
162
|
+
body_html: generated.body_html,
|
|
163
|
+
category: category ?? 'general',
|
|
164
|
+
description: 'Generated from MCP brief',
|
|
165
|
+
})
|
|
166
|
+
.select('id, name, subject_hint, created_at')
|
|
167
|
+
.single();
|
|
168
|
+
if (error)
|
|
169
|
+
return j({ error: error.message });
|
|
170
|
+
return j({ ok: true, template, preview: generated });
|
|
171
|
+
});
|
|
172
|
+
server.registerTool('diagnose_delivery_issue', {
|
|
173
|
+
description: 'Run a delivery audit across cron health, queue status, send DLQ, and domain health, then summarize likely root causes.',
|
|
174
|
+
inputSchema: {
|
|
175
|
+
site_id: z.string().uuid().optional(),
|
|
176
|
+
symptom: z.string().optional(),
|
|
177
|
+
},
|
|
178
|
+
}, async ({ site_id, symptom }) => {
|
|
179
|
+
const projectId = getProjectId();
|
|
180
|
+
const sb = db();
|
|
181
|
+
const since = new Date(Date.now() - 24 * 3600 * 1000).toISOString();
|
|
182
|
+
const cronQuery = sb.from('cron_runs')
|
|
183
|
+
.select('job_name, status, error_message, started_at, finished_at, items_failed')
|
|
184
|
+
.or(`project_id.is.null,project_id.eq.${projectId}`)
|
|
185
|
+
.gte('started_at', since)
|
|
186
|
+
.order('started_at', { ascending: false })
|
|
187
|
+
.limit(30);
|
|
188
|
+
const queueQuery = sb.from('send_queue').select('status').eq('project_id', projectId);
|
|
189
|
+
const dlqQuery = sb.from('send_dlq').select('final_error, attempts, created_at, resolved').eq('project_id', projectId).eq('resolved', false).order('created_at', { ascending: false }).limit(10);
|
|
190
|
+
let domainHealthQuery = sb.from('domain_health').select('site_id, spf_valid, dkim_valid, dmarc_valid, check_date, bounce_rate, complaint_rate, warmup_day').order('check_date', { ascending: false });
|
|
191
|
+
if (site_id)
|
|
192
|
+
domainHealthQuery = domainHealthQuery.eq('site_id', site_id);
|
|
193
|
+
else {
|
|
194
|
+
const { data: sites } = await sb.from('sites').select('id').eq('project_id', projectId);
|
|
195
|
+
const ids = (sites ?? []).map((row) => row.id);
|
|
196
|
+
if (ids.length)
|
|
197
|
+
domainHealthQuery = domainHealthQuery.in('site_id', ids);
|
|
198
|
+
}
|
|
199
|
+
const [cronRuns, queueStatus, dlq, domainHealth] = await Promise.all([cronQuery, queueQuery, dlqQuery, domainHealthQuery]);
|
|
200
|
+
const cronFailures = (cronRuns.data ?? []).filter((row) => row.status === 'failed');
|
|
201
|
+
const queueCounts = {};
|
|
202
|
+
for (const row of queueStatus.data ?? [])
|
|
203
|
+
queueCounts[row.status] = (queueCounts[row.status] ?? 0) + 1;
|
|
204
|
+
const domainIssues = (domainHealth.data ?? []).filter((row) => row.spf_valid !== true || row.dkim_valid !== true || row.dmarc_valid !== true);
|
|
205
|
+
const diagnosticData = {
|
|
206
|
+
cron: { total_recent: cronRuns.data?.length ?? 0, failures: cronFailures.length, failed_jobs: cronFailures },
|
|
207
|
+
queue: queueCounts,
|
|
208
|
+
dlq: { unresolved_count: dlq.data?.length ?? 0, samples: (dlq.data ?? []).slice(0, 3) },
|
|
209
|
+
domain_health: { sites_with_issues: domainIssues.length, issues: domainIssues.slice(0, 5) },
|
|
210
|
+
};
|
|
211
|
+
const analysisMsg = await getAnthropicClient().messages.create({
|
|
212
|
+
model: getAnthropicFastModelId(),
|
|
213
|
+
max_tokens: 1024,
|
|
214
|
+
messages: [{
|
|
215
|
+
role: 'user',
|
|
216
|
+
content: `You are a deliverability engineer. Diagnose this email delivery issue.
|
|
217
|
+
${symptom ? `REPORTED SYMPTOM: ${symptom}` : ''}
|
|
218
|
+
|
|
219
|
+
DIAGNOSTIC DATA:
|
|
220
|
+
${JSON.stringify(diagnosticData, null, 2)}
|
|
221
|
+
|
|
222
|
+
Identify the most likely root cause and give 3-5 concise actionable next steps.`,
|
|
223
|
+
}],
|
|
224
|
+
});
|
|
225
|
+
const analysis = analysisMsg.content[0].text;
|
|
226
|
+
return j({
|
|
227
|
+
diagnostic_data: diagnosticData,
|
|
228
|
+
analysis,
|
|
229
|
+
severity: cronFailures.length > 0 ? 'high' : domainIssues.length > 0 || (dlq.data?.length ?? 0) > 0 ? 'medium' : 'low',
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
server.registerTool('onboard_new_site', {
|
|
233
|
+
description: 'Create a new site, register a sending domain, and return warmup guidance plus a next-step checklist.',
|
|
234
|
+
inputSchema: {
|
|
235
|
+
name: z.string().min(1),
|
|
236
|
+
domain: z.string().min(1),
|
|
237
|
+
provider: z.enum(['resend', 'sendgrid', 'ses', 'postmark']).default('resend'),
|
|
238
|
+
daily_send_volume: z.number().int().min(1).default(100),
|
|
239
|
+
brand_settings: z.object({
|
|
240
|
+
mission: z.string().optional(),
|
|
241
|
+
tone_words: z.array(z.string()).optional(),
|
|
242
|
+
signoff_name: z.string().optional(),
|
|
243
|
+
}).optional(),
|
|
244
|
+
},
|
|
245
|
+
}, async ({ name, domain, provider, daily_send_volume, brand_settings }) => {
|
|
246
|
+
const projectId = getProjectId();
|
|
247
|
+
const sb = db();
|
|
248
|
+
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'site';
|
|
249
|
+
const fromName = brand_settings?.signoff_name ?? name;
|
|
250
|
+
const fromEmail = `hello@${domain}`;
|
|
251
|
+
const { data: createdDomain, error: domainErr } = await sb.from('domains')
|
|
252
|
+
.upsert({
|
|
253
|
+
project_id: projectId,
|
|
254
|
+
name: domain,
|
|
255
|
+
provider,
|
|
256
|
+
region: 'us-east-1',
|
|
257
|
+
status: 'pending',
|
|
258
|
+
dns_records: [
|
|
259
|
+
{ type: 'TXT', name: domain, value: 'v=spf1 include:amazonses.com ~all', required: true },
|
|
260
|
+
{ type: 'TXT', name: `_dmarc.${domain}`, value: 'v=DMARC1; p=none; rua=mailto:dmarc@sendinel.ai', required: false },
|
|
261
|
+
],
|
|
262
|
+
}, { onConflict: 'project_id,name' })
|
|
263
|
+
.select('id, name, provider, status, dns_records')
|
|
264
|
+
.single();
|
|
265
|
+
if (domainErr)
|
|
266
|
+
return j({ error: `Domain registration failed: ${domainErr.message}` });
|
|
267
|
+
const { data: site, error: siteErr } = await sb.from('sites')
|
|
268
|
+
.insert({
|
|
269
|
+
project_id: projectId,
|
|
270
|
+
name,
|
|
271
|
+
slug,
|
|
272
|
+
provider,
|
|
273
|
+
sending_domain: domain,
|
|
274
|
+
from_name: fromName,
|
|
275
|
+
from_email: fromEmail,
|
|
276
|
+
active: true,
|
|
277
|
+
domain_id: createdDomain.id,
|
|
278
|
+
brand_voice: brand_settings?.tone_words ? { tone_words: brand_settings.tone_words, mission: brand_settings.mission } : {},
|
|
279
|
+
})
|
|
280
|
+
.select('id, name, slug, sending_domain, from_name, from_email, provider, created_at')
|
|
281
|
+
.single();
|
|
282
|
+
if (siteErr)
|
|
283
|
+
return j({ error: `Site creation failed: ${siteErr.message}` });
|
|
284
|
+
const warmupPlan = [];
|
|
285
|
+
let volume = Math.max(10, Math.floor(daily_send_volume * 0.05));
|
|
286
|
+
for (let day = 1; day <= 14; day += 3) {
|
|
287
|
+
warmupPlan.push({ day, volume: Math.min(volume, daily_send_volume) });
|
|
288
|
+
volume = Math.min(volume * 2, daily_send_volume);
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
await sb.from('warmup_schedules')
|
|
292
|
+
.upsert({ site_id: site.id, project_id: projectId, target_volume: daily_send_volume, schedule: warmupPlan, status: 'active', created_at: new Date().toISOString() }, { onConflict: 'site_id' });
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
// warmup_schedules may not exist; this is non-blocking
|
|
296
|
+
}
|
|
297
|
+
return j({
|
|
298
|
+
ok: true,
|
|
299
|
+
site,
|
|
300
|
+
domain: createdDomain,
|
|
301
|
+
warmup_plan: warmupPlan,
|
|
302
|
+
checklist: [
|
|
303
|
+
`Site "${name}" created (id: ${site.id})`,
|
|
304
|
+
'Add the required DNS records for the sending domain',
|
|
305
|
+
'Wait for DNS propagation, then run get_domain_health',
|
|
306
|
+
`Configure the ${provider} provider key before sending`,
|
|
307
|
+
'Send a test email after DNS and provider setup are complete',
|
|
308
|
+
],
|
|
309
|
+
next_step: `Run get_domain_health with site_id "${site.id}" after DNS records are in place.`,
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
}
|