@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,68 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { db } from '../db.js';
|
|
3
|
+
import { getProjectId } from '../project.js';
|
|
4
|
+
const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
5
|
+
export function registerContentTools(server) {
|
|
6
|
+
server.registerTool('list_content_blocks', {
|
|
7
|
+
description: 'List reusable content blocks for the project, optionally filtered by category.',
|
|
8
|
+
inputSchema: { category: z.enum(['header', 'footer', 'cta', 'legal', 'general']).optional() },
|
|
9
|
+
}, async ({ category }) => {
|
|
10
|
+
let query = db().from('content_blocks').select('id, name, description, category, html, updated_at').eq('project_id', getProjectId()).order('updated_at', { ascending: false });
|
|
11
|
+
if (category)
|
|
12
|
+
query = query.eq('category', category);
|
|
13
|
+
const { data, error } = await query;
|
|
14
|
+
if (error)
|
|
15
|
+
return j({ error: error.message });
|
|
16
|
+
return j({ blocks: data ?? [] });
|
|
17
|
+
});
|
|
18
|
+
server.registerTool('create_content_block', {
|
|
19
|
+
description: 'Create a reusable content block.',
|
|
20
|
+
inputSchema: {
|
|
21
|
+
name: z.string().min(1),
|
|
22
|
+
category: z.enum(['header', 'footer', 'cta', 'legal', 'general']).optional().default('general'),
|
|
23
|
+
html: z.string().min(1),
|
|
24
|
+
description: z.string().optional(),
|
|
25
|
+
},
|
|
26
|
+
}, async ({ name, category = 'general', html, description }) => {
|
|
27
|
+
const { data, error } = await db().from('content_blocks')
|
|
28
|
+
.insert({ project_id: getProjectId(), name, category, html, description: description ?? null })
|
|
29
|
+
.select('id, name, category, html, description, created_at')
|
|
30
|
+
.single();
|
|
31
|
+
if (error)
|
|
32
|
+
return j({ error: error.message });
|
|
33
|
+
return j({ block: data });
|
|
34
|
+
});
|
|
35
|
+
server.registerTool('delete_content_block', {
|
|
36
|
+
description: 'Delete a reusable content block.',
|
|
37
|
+
inputSchema: { block_id: z.string().uuid() },
|
|
38
|
+
}, async ({ block_id }) => {
|
|
39
|
+
const { error } = await db().from('content_blocks').delete().eq('project_id', getProjectId()).eq('id', block_id);
|
|
40
|
+
if (error)
|
|
41
|
+
return j({ error: error.message });
|
|
42
|
+
return j({ deleted: true });
|
|
43
|
+
});
|
|
44
|
+
server.registerTool('list_content_sources', {
|
|
45
|
+
description: 'List configured content sources and draft counts.',
|
|
46
|
+
inputSchema: {},
|
|
47
|
+
}, async () => {
|
|
48
|
+
const { data, error } = await db().from('content_sources')
|
|
49
|
+
.select('id, name, source_type, feed_url, active, last_polled_at, items_generated')
|
|
50
|
+
.eq('project_id', getProjectId())
|
|
51
|
+
.order('created_at', { ascending: false });
|
|
52
|
+
if (error)
|
|
53
|
+
return j({ error: error.message });
|
|
54
|
+
return j({ sources: (data ?? []).map((row) => ({ id: row.id, name: row.name, type: row.source_type, url: row.feed_url, active: row.active, last_fetched: row.last_polled_at, draft_count: row.items_generated ?? 0 })) });
|
|
55
|
+
});
|
|
56
|
+
server.registerTool('trigger_content_pipeline', {
|
|
57
|
+
description: 'Mark one or all content sources ready for the content pipeline cron.',
|
|
58
|
+
inputSchema: { source_id: z.string().uuid().optional() },
|
|
59
|
+
}, async ({ source_id }) => {
|
|
60
|
+
let query = db().from('content_sources').update({ last_polled_at: null }).eq('project_id', getProjectId());
|
|
61
|
+
if (source_id)
|
|
62
|
+
query = query.eq('id', source_id);
|
|
63
|
+
const { error } = await query;
|
|
64
|
+
if (error)
|
|
65
|
+
return j({ error: error.message });
|
|
66
|
+
return j({ drafts_created: 0, queued_for_pipeline: true });
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { db } from '../db.js';
|
|
3
|
+
import { getProjectId } from '../project.js';
|
|
4
|
+
import { getAnthropicModelId } from '../anthropic-model.js';
|
|
5
|
+
const txt = (text) => ({ content: [{ type: 'text', text }] });
|
|
6
|
+
const j = (data) => txt(JSON.stringify(data, null, 2));
|
|
7
|
+
async function engagementSnapshot(projectId) {
|
|
8
|
+
const since = new Date(Date.now() - 7 * 86400000).toISOString();
|
|
9
|
+
const [{ data: logs }, { count: audienceSize }, { data: segments }] = await Promise.all([
|
|
10
|
+
db().from('email_log').select('status, created_at').eq('project_id', projectId).gte('created_at', since).limit(10000),
|
|
11
|
+
db().from('subscribers').select('id', { count: 'exact', head: true }).eq('project_id', projectId).eq('is_test', false).eq('global_unsubscribed', false),
|
|
12
|
+
db().from('segments').select('id, name, subscriber_count, updated_at').eq('project_id', projectId).order('updated_at', { ascending: false }).limit(5),
|
|
13
|
+
]);
|
|
14
|
+
const rows = logs ?? [];
|
|
15
|
+
const sent = rows.filter((row) => ['sent', 'delivered', 'opened', 'clicked', 'bounced', 'complained'].includes(row.status)).length;
|
|
16
|
+
const opened = rows.filter((row) => row.status === 'opened' || row.status === 'clicked').length;
|
|
17
|
+
const clicked = rows.filter((row) => row.status === 'clicked').length;
|
|
18
|
+
return {
|
|
19
|
+
period_days: 7,
|
|
20
|
+
sent,
|
|
21
|
+
open_rate: sent > 0 ? Number((opened / sent).toFixed(4)) : 0,
|
|
22
|
+
click_rate: sent > 0 ? Number((clicked / sent).toFixed(4)) : 0,
|
|
23
|
+
audience_size: audienceSize ?? 0,
|
|
24
|
+
recent_segments: segments ?? [],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function registerDataProposalTools(server) {
|
|
28
|
+
server.registerTool('propose_campaign_from_data', {
|
|
29
|
+
description: 'Create an approve-gated campaign proposal from recent engagement signals and project Brand Brain.',
|
|
30
|
+
inputSchema: {
|
|
31
|
+
source_type: z.enum(['engagement_signal', 'segment_trend', 'cross_channel_synthesis']).optional().default('engagement_signal'),
|
|
32
|
+
segment_id: z.string().uuid().optional(),
|
|
33
|
+
},
|
|
34
|
+
}, async ({ source_type, segment_id }) => {
|
|
35
|
+
const projectId = getProjectId();
|
|
36
|
+
const { data: project, error: projectError } = await db()
|
|
37
|
+
.from('projects')
|
|
38
|
+
.select('id, org_id, owner_user_id, name, brand_settings')
|
|
39
|
+
.eq('id', projectId)
|
|
40
|
+
.maybeSingle();
|
|
41
|
+
if (projectError)
|
|
42
|
+
return txt(`Error: ${projectError.message}`);
|
|
43
|
+
if (!project)
|
|
44
|
+
return txt('Project not found.');
|
|
45
|
+
const snapshot = await engagementSnapshot(projectId);
|
|
46
|
+
const audience = project.brand_settings?.audience ?? 'subscribers';
|
|
47
|
+
const title = `Re-engage ${audience}`;
|
|
48
|
+
const subject = `A useful update from ${project.name}`;
|
|
49
|
+
const body = `Here is a timely update for ${audience}.\n\nRecent engagement suggests subscribers need one clear next step.\n\n{{cta_url|Read the update}}`;
|
|
50
|
+
const { data, error } = await db()
|
|
51
|
+
.from('data_proposals')
|
|
52
|
+
.insert({
|
|
53
|
+
org_id: project.org_id,
|
|
54
|
+
project_id: projectId,
|
|
55
|
+
user_id: project.owner_user_id,
|
|
56
|
+
source_type,
|
|
57
|
+
engagement_snapshot: snapshot,
|
|
58
|
+
campaign_title: title,
|
|
59
|
+
campaign_subject: subject,
|
|
60
|
+
campaign_body_draft: body,
|
|
61
|
+
campaign_cta: 'Read the update',
|
|
62
|
+
target_segment_id: segment_id ?? null,
|
|
63
|
+
target_audience_size: snapshot.audience_size,
|
|
64
|
+
premises: `Recent engagement data: ${JSON.stringify(snapshot)}`,
|
|
65
|
+
execution_trace: 'MCP proposal generated from first-party engagement snapshot and project Brand Brain.',
|
|
66
|
+
brand_voice_used: project.brand_settings ?? {},
|
|
67
|
+
})
|
|
68
|
+
.select('*')
|
|
69
|
+
.single();
|
|
70
|
+
if (error)
|
|
71
|
+
return txt(`Error: ${error.message}`);
|
|
72
|
+
return j({ proposal: data });
|
|
73
|
+
});
|
|
74
|
+
server.registerTool('list_data_proposals', {
|
|
75
|
+
description: 'List data-triggered campaign proposals for the current project.',
|
|
76
|
+
inputSchema: {
|
|
77
|
+
status: z.enum(['pending', 'approved', 'rejected', 'converted_to_draft', 'expired']).optional(),
|
|
78
|
+
limit: z.number().int().min(1).max(100).optional().default(25),
|
|
79
|
+
},
|
|
80
|
+
}, async ({ status, limit }) => {
|
|
81
|
+
let query = db()
|
|
82
|
+
.from('data_proposals')
|
|
83
|
+
.select('*')
|
|
84
|
+
.eq('project_id', getProjectId())
|
|
85
|
+
.order('created_at', { ascending: false })
|
|
86
|
+
.limit(limit ?? 25);
|
|
87
|
+
if (status)
|
|
88
|
+
query = query.eq('status', status);
|
|
89
|
+
const { data, error } = await query;
|
|
90
|
+
if (error)
|
|
91
|
+
return txt(`Error: ${error.message}`);
|
|
92
|
+
return j({ proposals: data ?? [] });
|
|
93
|
+
});
|
|
94
|
+
server.registerTool('approve_data_proposal', {
|
|
95
|
+
description: 'Approve a data-triggered campaign proposal and convert it into an AI draft for review.',
|
|
96
|
+
inputSchema: {
|
|
97
|
+
proposal_id: z.string().uuid(),
|
|
98
|
+
},
|
|
99
|
+
}, async ({ proposal_id }) => {
|
|
100
|
+
const projectId = getProjectId();
|
|
101
|
+
const { data: proposal, error: proposalError } = await db()
|
|
102
|
+
.from('data_proposals')
|
|
103
|
+
.select('*')
|
|
104
|
+
.eq('id', proposal_id)
|
|
105
|
+
.eq('project_id', projectId)
|
|
106
|
+
.maybeSingle();
|
|
107
|
+
if (proposalError)
|
|
108
|
+
return txt(`Error: ${proposalError.message}`);
|
|
109
|
+
if (!proposal)
|
|
110
|
+
return txt('Proposal not found.');
|
|
111
|
+
if (!['pending', 'approved'].includes(proposal.status))
|
|
112
|
+
return txt('Proposal is not approvable.');
|
|
113
|
+
const { data: site, error: siteError } = await db()
|
|
114
|
+
.from('sites')
|
|
115
|
+
.select('id')
|
|
116
|
+
.eq('project_id', projectId)
|
|
117
|
+
.eq('active', true)
|
|
118
|
+
.order('created_at', { ascending: true })
|
|
119
|
+
.limit(1)
|
|
120
|
+
.maybeSingle();
|
|
121
|
+
if (siteError)
|
|
122
|
+
return txt(`Error: ${siteError.message}`);
|
|
123
|
+
if (!site)
|
|
124
|
+
return txt('No active site available for draft conversion.');
|
|
125
|
+
const { data: draft, error: draftError } = await db()
|
|
126
|
+
.from('ai_drafts')
|
|
127
|
+
.insert({
|
|
128
|
+
project_id: projectId,
|
|
129
|
+
site_id: site.id,
|
|
130
|
+
subject: proposal.campaign_subject ?? proposal.campaign_title,
|
|
131
|
+
body_html: String(proposal.campaign_body_draft).replace(/\n/g, '<br>'),
|
|
132
|
+
body_text: proposal.campaign_body_draft,
|
|
133
|
+
prompt: `[Data proposal] ${proposal.premises}`,
|
|
134
|
+
model: getAnthropicModelId(),
|
|
135
|
+
})
|
|
136
|
+
.select('id')
|
|
137
|
+
.single();
|
|
138
|
+
if (draftError)
|
|
139
|
+
return txt(`Error: ${draftError.message}`);
|
|
140
|
+
const { data: updated, error: updateError } = await db()
|
|
141
|
+
.from('data_proposals')
|
|
142
|
+
.update({
|
|
143
|
+
status: 'converted_to_draft',
|
|
144
|
+
approved_at: new Date().toISOString(),
|
|
145
|
+
converted_to_draft_id: draft.id,
|
|
146
|
+
})
|
|
147
|
+
.eq('id', proposal.id)
|
|
148
|
+
.eq('project_id', projectId)
|
|
149
|
+
.select('*')
|
|
150
|
+
.single();
|
|
151
|
+
if (updateError)
|
|
152
|
+
return txt(`Error: ${updateError.message}`);
|
|
153
|
+
return j({ proposal: updated, draft_id: draft.id });
|
|
154
|
+
});
|
|
155
|
+
}
|