@sendinel/mcp-server 1.0.4 → 1.0.6
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/build/audit.d.ts +1 -0
- package/build/audit.js +9 -3
- package/build/auth.d.ts +10 -0
- package/build/auth.js +82 -9
- package/build/db.d.ts +12 -0
- package/build/db.js +37 -2
- package/build/index.js +38 -12
- package/build/lib/action-approvals.d.ts +28 -1
- package/build/lib/action-approvals.js +89 -2
- package/build/lib/brand-asset-persist.d.ts +35 -0
- package/build/lib/brand-asset-persist.js +161 -0
- package/build/lib/brand-asset-usage.d.ts +13 -0
- package/build/lib/brand-asset-usage.js +46 -0
- package/build/lib/email-provider.d.ts +19 -0
- package/build/lib/email-provider.js +281 -0
- package/build/lib/ssrf-pinned-fetch.d.ts +16 -0
- package/build/lib/ssrf-pinned-fetch.js +101 -0
- package/build/lib/survey-link-resolver.d.ts +6 -0
- package/build/lib/survey-link-resolver.js +23 -0
- package/build/lib/svg-sanitize.d.ts +2 -0
- package/build/lib/svg-sanitize.js +110 -0
- package/build/lib/webhook-events.d.ts +1 -1
- package/build/scoped-token.d.ts +11 -0
- package/build/scoped-token.js +59 -0
- package/build/server.js +30 -9
- package/build/tools/ab-testing.js +28 -0
- package/build/tools/advisor.js +52 -2
- package/build/tools/ai-work.d.ts +2 -0
- package/build/tools/ai-work.js +460 -0
- package/build/tools/analytics.d.ts +1 -0
- package/build/tools/analytics.js +203 -0
- package/build/tools/approvals.js +15 -1
- package/build/tools/automations.js +288 -65
- package/build/tools/brand-kit.d.ts +2 -0
- package/build/tools/brand-kit.js +277 -0
- package/build/tools/campaigns.js +900 -69
- package/build/tools/compound.js +28 -0
- package/build/tools/contacts.js +338 -17
- package/build/tools/content.js +266 -1
- package/build/tools/data-proposals.js +21 -0
- package/build/tools/data.js +140 -8
- package/build/tools/delivery-ops.js +84 -0
- package/build/tools/drafts.js +64 -1
- package/build/tools/forms.js +42 -9
- package/build/tools/gdpr.js +22 -3
- package/build/tools/newsletters.d.ts +2 -0
- package/build/tools/newsletters.js +583 -0
- package/build/tools/org.js +297 -3
- package/build/tools/segments.js +45 -3
- package/build/tools/short-links.d.ts +2 -0
- package/build/tools/short-links.js +157 -0
- package/build/tools/sites.js +35 -0
- package/build/tools/sms.js +49 -0
- package/build/tools/social-posts.js +81 -5
- package/build/tools/surveys.d.ts +2 -0
- package/build/tools/surveys.js +222 -0
- package/build/tools/templates.js +78 -2
- package/build/tools/warmup.js +28 -0
- package/build/tools/webhooks.js +29 -0
- package/package.json +9 -3
package/build/tools/content.js
CHANGED
|
@@ -1,9 +1,113 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { db } from '../db.js';
|
|
2
|
+
import { adminDb, db } from '../db.js';
|
|
3
3
|
import { getProjectId } from '../project.js';
|
|
4
4
|
const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
5
5
|
export function registerContentTools(server) {
|
|
6
|
+
server.registerTool('create_asset_link', {
|
|
7
|
+
title: 'Create Asset Link',
|
|
8
|
+
annotations: {
|
|
9
|
+
readOnlyHint: false,
|
|
10
|
+
destructiveHint: false,
|
|
11
|
+
idempotentHint: false,
|
|
12
|
+
openWorldHint: false,
|
|
13
|
+
},
|
|
14
|
+
description: 'Create an email-gated, trackable asset link. Viewers submit an email at /a/{slug}; each view creates or attaches a contact and emits asset_link.viewed.',
|
|
15
|
+
inputSchema: {
|
|
16
|
+
title: z.string().min(1).max(255),
|
|
17
|
+
slug: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).max(100),
|
|
18
|
+
asset_url: z.string().url().max(2000),
|
|
19
|
+
asset_type: z.enum(['pdf', 'page', 'deck', 'video', 'other']).optional().default('page'),
|
|
20
|
+
site_id: z.string().uuid().optional(),
|
|
21
|
+
source_attribution: z.string().max(500).optional(),
|
|
22
|
+
gate_headline: z.string().max(255).optional(),
|
|
23
|
+
gate_body: z.string().max(1000).optional(),
|
|
24
|
+
},
|
|
25
|
+
}, async ({ title, slug, asset_url, asset_type = 'page', site_id, source_attribution, gate_headline, gate_body }) => {
|
|
26
|
+
const { data, error } = await db().from('asset_links')
|
|
27
|
+
.insert({
|
|
28
|
+
project_id: getProjectId(),
|
|
29
|
+
site_id: site_id ?? null,
|
|
30
|
+
title,
|
|
31
|
+
slug,
|
|
32
|
+
asset_url,
|
|
33
|
+
asset_type,
|
|
34
|
+
source_attribution: source_attribution ?? null,
|
|
35
|
+
gate_headline: gate_headline ?? null,
|
|
36
|
+
gate_body: gate_body ?? null,
|
|
37
|
+
})
|
|
38
|
+
.select('id, title, slug, asset_type, status, source_attribution, gate_headline, gate_body, total_views, total_captures, created_at')
|
|
39
|
+
.single();
|
|
40
|
+
if (error)
|
|
41
|
+
return j({ error: error.message, code: error.code });
|
|
42
|
+
const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? process.env.SENDINEL_APP_URL ?? 'https://sendinel.ai';
|
|
43
|
+
return j({ asset_link: { ...data, url: `${appUrl.replace(/\/$/, '')}/a/${data.slug}` } });
|
|
44
|
+
});
|
|
45
|
+
server.registerTool('list_asset_links', {
|
|
46
|
+
title: 'List Asset Links',
|
|
47
|
+
annotations: {
|
|
48
|
+
readOnlyHint: true,
|
|
49
|
+
destructiveHint: false,
|
|
50
|
+
idempotentHint: true,
|
|
51
|
+
openWorldHint: false,
|
|
52
|
+
},
|
|
53
|
+
description: 'List email-gated asset links and aggregate capture/view counters.',
|
|
54
|
+
inputSchema: {
|
|
55
|
+
status: z.enum(['draft', 'active', 'archived']).optional(),
|
|
56
|
+
limit: z.number().int().min(1).max(200).optional().default(50),
|
|
57
|
+
},
|
|
58
|
+
}, async ({ status, limit = 50 }) => {
|
|
59
|
+
let query = db().from('asset_links')
|
|
60
|
+
.select('id, title, slug, asset_type, status, source_attribution, total_views, total_captures, created_at, updated_at')
|
|
61
|
+
.eq('project_id', getProjectId())
|
|
62
|
+
.order('created_at', { ascending: false })
|
|
63
|
+
.limit(limit);
|
|
64
|
+
if (status)
|
|
65
|
+
query = query.eq('status', status);
|
|
66
|
+
const { data, error } = await query;
|
|
67
|
+
if (error)
|
|
68
|
+
return j({ error: error.message });
|
|
69
|
+
const appUrl = (process.env.NEXT_PUBLIC_APP_URL ?? process.env.SENDINEL_APP_URL ?? 'https://sendinel.ai').replace(/\/$/, '');
|
|
70
|
+
return j({ asset_links: (data ?? []).map((row) => ({ ...row, url: `${appUrl}/a/${row.slug}` })) });
|
|
71
|
+
});
|
|
72
|
+
server.registerTool('get_asset_link_views', {
|
|
73
|
+
title: 'Get Asset Link Views',
|
|
74
|
+
annotations: {
|
|
75
|
+
readOnlyHint: true,
|
|
76
|
+
destructiveHint: false,
|
|
77
|
+
idempotentHint: true,
|
|
78
|
+
openWorldHint: false,
|
|
79
|
+
},
|
|
80
|
+
description: 'List per-view events for a gated asset link, including captured email, contact id, and duration when available.',
|
|
81
|
+
inputSchema: {
|
|
82
|
+
asset_link_id: z.string().uuid(),
|
|
83
|
+
limit: z.number().int().min(1).max(200).optional().default(50),
|
|
84
|
+
},
|
|
85
|
+
}, async ({ asset_link_id, limit = 50 }) => {
|
|
86
|
+
const { data: assetLink } = await db().from('asset_links')
|
|
87
|
+
.select('id')
|
|
88
|
+
.eq('id', asset_link_id)
|
|
89
|
+
.eq('project_id', getProjectId())
|
|
90
|
+
.maybeSingle();
|
|
91
|
+
if (!assetLink)
|
|
92
|
+
return j({ error: 'Asset link not found', code: 'NOT_FOUND' });
|
|
93
|
+
const { data, error } = await db().from('asset_link_views')
|
|
94
|
+
.select('id, contact_id, viewer_email, is_new_contact, source_attribution, viewed_at, left_at, duration_seconds, referrer')
|
|
95
|
+
.eq('asset_link_id', asset_link_id)
|
|
96
|
+
.eq('project_id', getProjectId())
|
|
97
|
+
.order('viewed_at', { ascending: false })
|
|
98
|
+
.limit(limit);
|
|
99
|
+
if (error)
|
|
100
|
+
return j({ error: error.message });
|
|
101
|
+
return j({ views: data ?? [] });
|
|
102
|
+
});
|
|
6
103
|
server.registerTool('list_content_blocks', {
|
|
104
|
+
title: 'List Content Blocks',
|
|
105
|
+
annotations: {
|
|
106
|
+
readOnlyHint: true,
|
|
107
|
+
destructiveHint: false,
|
|
108
|
+
idempotentHint: true,
|
|
109
|
+
openWorldHint: false,
|
|
110
|
+
},
|
|
7
111
|
description: 'List reusable content blocks for the project, optionally filtered by category.',
|
|
8
112
|
inputSchema: { category: z.enum(['header', 'footer', 'cta', 'legal', 'general']).optional() },
|
|
9
113
|
}, async ({ category }) => {
|
|
@@ -16,6 +120,13 @@ export function registerContentTools(server) {
|
|
|
16
120
|
return j({ blocks: data ?? [] });
|
|
17
121
|
});
|
|
18
122
|
server.registerTool('create_content_block', {
|
|
123
|
+
title: 'Create Content Block',
|
|
124
|
+
annotations: {
|
|
125
|
+
readOnlyHint: false,
|
|
126
|
+
destructiveHint: false,
|
|
127
|
+
idempotentHint: false,
|
|
128
|
+
openWorldHint: false,
|
|
129
|
+
},
|
|
19
130
|
description: 'Create a reusable content block.',
|
|
20
131
|
inputSchema: {
|
|
21
132
|
name: z.string().min(1),
|
|
@@ -33,6 +144,13 @@ export function registerContentTools(server) {
|
|
|
33
144
|
return j({ block: data });
|
|
34
145
|
});
|
|
35
146
|
server.registerTool('delete_content_block', {
|
|
147
|
+
title: 'Delete Content Block',
|
|
148
|
+
annotations: {
|
|
149
|
+
readOnlyHint: false,
|
|
150
|
+
destructiveHint: true,
|
|
151
|
+
idempotentHint: false,
|
|
152
|
+
openWorldHint: false,
|
|
153
|
+
},
|
|
36
154
|
description: 'Delete a reusable content block.',
|
|
37
155
|
inputSchema: { block_id: z.string().uuid() },
|
|
38
156
|
}, async ({ block_id }) => {
|
|
@@ -42,6 +160,13 @@ export function registerContentTools(server) {
|
|
|
42
160
|
return j({ deleted: true });
|
|
43
161
|
});
|
|
44
162
|
server.registerTool('list_content_sources', {
|
|
163
|
+
title: 'List Content Sources',
|
|
164
|
+
annotations: {
|
|
165
|
+
readOnlyHint: true,
|
|
166
|
+
destructiveHint: false,
|
|
167
|
+
idempotentHint: true,
|
|
168
|
+
openWorldHint: false,
|
|
169
|
+
},
|
|
45
170
|
description: 'List configured content sources and draft counts.',
|
|
46
171
|
inputSchema: {},
|
|
47
172
|
}, async () => {
|
|
@@ -54,6 +179,13 @@ export function registerContentTools(server) {
|
|
|
54
179
|
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
180
|
});
|
|
56
181
|
server.registerTool('trigger_content_pipeline', {
|
|
182
|
+
title: 'Trigger Content Pipeline',
|
|
183
|
+
annotations: {
|
|
184
|
+
readOnlyHint: false,
|
|
185
|
+
destructiveHint: false,
|
|
186
|
+
idempotentHint: false,
|
|
187
|
+
openWorldHint: true,
|
|
188
|
+
},
|
|
57
189
|
description: 'Mark one or all content sources ready for the content pipeline cron.',
|
|
58
190
|
inputSchema: { source_id: z.string().uuid().optional() },
|
|
59
191
|
}, async ({ source_id }) => {
|
|
@@ -65,4 +197,137 @@ export function registerContentTools(server) {
|
|
|
65
197
|
return j({ error: error.message });
|
|
66
198
|
return j({ drafts_created: 0, queued_for_pipeline: true });
|
|
67
199
|
});
|
|
200
|
+
// Blogs (#2033): the primary agent door for ingesting a blog written by the
|
|
201
|
+
// user's LLM/agent. Lands as a draft staged for review; publishing to the
|
|
202
|
+
// user's own site is a separate step. Sender-owns-schema -- only title is
|
|
203
|
+
// required, and unmapped fields are preserved in source_payload.
|
|
204
|
+
const slugify = (s) => s.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, '').replace(/\s+/g, '-').replace(/-+/g, '-').slice(0, 120).replace(/^-|-$/g, '');
|
|
205
|
+
server.registerTool('create_blog_post', {
|
|
206
|
+
title: 'Create Blog Post',
|
|
207
|
+
annotations: {
|
|
208
|
+
readOnlyHint: false,
|
|
209
|
+
destructiveHint: false,
|
|
210
|
+
idempotentHint: false,
|
|
211
|
+
openWorldHint: true,
|
|
212
|
+
},
|
|
213
|
+
description: 'Ingest a blog post (written by your AI/agent) into Sendinel. By default it lands as a draft in the review queue. For a trusted, hands-off flow, set auto_publish=true with publish_to (a destination name or id from list_blog_destinations) to skip review and publish to the user\'s own site — now, or at scheduled_at. Only title is required; everything else is optional.',
|
|
214
|
+
inputSchema: {
|
|
215
|
+
title: z.string().min(1).max(300),
|
|
216
|
+
body: z.string().optional(),
|
|
217
|
+
excerpt: z.string().max(1000).optional(),
|
|
218
|
+
author: z.string().max(120).optional(),
|
|
219
|
+
slug: z.string().max(120).optional(),
|
|
220
|
+
tags: z.array(z.string().max(64)).max(30).optional(),
|
|
221
|
+
cover_image_url: z.string().url().max(2000).optional(),
|
|
222
|
+
site_id: z.string().uuid().optional(),
|
|
223
|
+
seo: z.record(z.string(), z.unknown()).optional(),
|
|
224
|
+
publish_destination: z.record(z.string(), z.unknown()).optional(),
|
|
225
|
+
scheduled_at: z.string().datetime().optional(),
|
|
226
|
+
auto_publish: z.boolean().optional(),
|
|
227
|
+
publish_to: z.string().max(200).optional(),
|
|
228
|
+
},
|
|
229
|
+
}, async (args) => {
|
|
230
|
+
const { data: proj, error: projErr } = await adminDb().from('projects').select('org_id').eq('id', getProjectId()).single();
|
|
231
|
+
if (projErr || !proj?.org_id)
|
|
232
|
+
return j({ error: 'Organization not found for project' });
|
|
233
|
+
const { title, body, excerpt, author, slug, tags, cover_image_url, site_id, seo, publish_destination, scheduled_at, auto_publish, publish_to } = args;
|
|
234
|
+
// One-shot auto-publish: skip the review queue and hand the post straight to
|
|
235
|
+
// the publish worker (status='scheduled', due now unless scheduled_at is set).
|
|
236
|
+
// Requires a resolvable, active destination so a typo can't silently drop the
|
|
237
|
+
// post into a limbo state.
|
|
238
|
+
let status = 'draft';
|
|
239
|
+
let resolvedScheduledAt = scheduled_at ?? null;
|
|
240
|
+
let resolvedDestination = publish_destination ?? {};
|
|
241
|
+
if (auto_publish) {
|
|
242
|
+
if (!publish_to) {
|
|
243
|
+
return j({ error: 'auto_publish requires publish_to (a destination name or id). Call list_blog_destinations to see the options.' });
|
|
244
|
+
}
|
|
245
|
+
const { data: dests, error: destErr } = await db().from('blog_destinations')
|
|
246
|
+
.select('id, name')
|
|
247
|
+
.eq('project_id', getProjectId())
|
|
248
|
+
.eq('is_active', true);
|
|
249
|
+
if (destErr)
|
|
250
|
+
return j({ error: destErr.message });
|
|
251
|
+
const target = publish_to.trim().toLowerCase();
|
|
252
|
+
const match = (dests ?? []).find((d) => d.id === publish_to || String(d.name ?? '').toLowerCase() === target);
|
|
253
|
+
if (!match) {
|
|
254
|
+
return j({ error: `No active publish destination matching "${publish_to}". Add one on the Blog page, or call list_blog_destinations.` });
|
|
255
|
+
}
|
|
256
|
+
status = 'scheduled';
|
|
257
|
+
resolvedScheduledAt = scheduled_at ?? new Date().toISOString();
|
|
258
|
+
resolvedDestination = { destination_id: match.id };
|
|
259
|
+
}
|
|
260
|
+
const { data, error } = await db().from('blog_posts')
|
|
261
|
+
.insert({
|
|
262
|
+
org_id: proj.org_id,
|
|
263
|
+
project_id: getProjectId(),
|
|
264
|
+
site_id: site_id ?? null,
|
|
265
|
+
title,
|
|
266
|
+
slug: slug ? slugify(slug) : slugify(title),
|
|
267
|
+
body: body ?? null,
|
|
268
|
+
excerpt: excerpt ?? null,
|
|
269
|
+
author: author ?? null,
|
|
270
|
+
tags: tags ?? [],
|
|
271
|
+
cover_image_url: cover_image_url ?? null,
|
|
272
|
+
seo: seo ?? {},
|
|
273
|
+
status,
|
|
274
|
+
source: 'mcp',
|
|
275
|
+
source_payload: args,
|
|
276
|
+
publish_destination: resolvedDestination,
|
|
277
|
+
scheduled_at: resolvedScheduledAt,
|
|
278
|
+
})
|
|
279
|
+
.select('id, title, slug, status, source, scheduled_at, created_at')
|
|
280
|
+
.single();
|
|
281
|
+
if (error)
|
|
282
|
+
return j({ error: error.message, code: error.code });
|
|
283
|
+
return j({ blog_post: data, ...(auto_publish ? { note: scheduled_at ? 'Scheduled — the publish worker will send it at scheduled_at.' : 'Queued for immediate publish — the worker publishes it within a few minutes.' } : {}) });
|
|
284
|
+
});
|
|
285
|
+
server.registerTool('list_blog_posts', {
|
|
286
|
+
title: 'List Blog Posts',
|
|
287
|
+
annotations: {
|
|
288
|
+
readOnlyHint: true,
|
|
289
|
+
destructiveHint: false,
|
|
290
|
+
idempotentHint: true,
|
|
291
|
+
openWorldHint: false,
|
|
292
|
+
},
|
|
293
|
+
description: 'List ingested blog posts for this project, newest first. Optional status filter.',
|
|
294
|
+
inputSchema: {
|
|
295
|
+
status: z.enum(['draft', 'scheduled', 'published', 'archived']).optional(),
|
|
296
|
+
limit: z.number().int().min(1).max(200).optional().default(50),
|
|
297
|
+
},
|
|
298
|
+
}, async ({ status, limit = 50 }) => {
|
|
299
|
+
let query = db().from('blog_posts')
|
|
300
|
+
.select('id, title, slug, status, source, external_url, scheduled_at, published_at, created_at')
|
|
301
|
+
.eq('project_id', getProjectId())
|
|
302
|
+
.order('created_at', { ascending: false })
|
|
303
|
+
.limit(limit);
|
|
304
|
+
if (status)
|
|
305
|
+
query = query.eq('status', status);
|
|
306
|
+
else
|
|
307
|
+
query = query.neq('status', 'archived');
|
|
308
|
+
const { data, error } = await query;
|
|
309
|
+
if (error)
|
|
310
|
+
return j({ error: error.message });
|
|
311
|
+
return j({ blog_posts: data ?? [] });
|
|
312
|
+
});
|
|
313
|
+
server.registerTool('list_blog_destinations', {
|
|
314
|
+
title: 'List Blog Destinations',
|
|
315
|
+
annotations: {
|
|
316
|
+
readOnlyHint: true,
|
|
317
|
+
destructiveHint: false,
|
|
318
|
+
idempotentHint: true,
|
|
319
|
+
openWorldHint: false,
|
|
320
|
+
},
|
|
321
|
+
description: 'List this project\'s configured blog publish destinations (WordPress sites / signed webhooks) by id, name, and kind — pass a name or id as create_blog_post\'s publish_to to auto-publish. Never returns credentials.',
|
|
322
|
+
inputSchema: {},
|
|
323
|
+
}, async () => {
|
|
324
|
+
const { data, error } = await db().from('blog_destinations')
|
|
325
|
+
.select('id, name, kind, is_active')
|
|
326
|
+
.eq('project_id', getProjectId())
|
|
327
|
+
.eq('is_active', true)
|
|
328
|
+
.order('created_at', { ascending: false });
|
|
329
|
+
if (error)
|
|
330
|
+
return j({ error: error.message });
|
|
331
|
+
return j({ destinations: data ?? [] });
|
|
332
|
+
});
|
|
68
333
|
}
|
|
@@ -26,6 +26,13 @@ async function engagementSnapshot(projectId) {
|
|
|
26
26
|
}
|
|
27
27
|
export function registerDataProposalTools(server) {
|
|
28
28
|
server.registerTool('propose_campaign_from_data', {
|
|
29
|
+
title: 'Propose Campaign From Data',
|
|
30
|
+
annotations: {
|
|
31
|
+
readOnlyHint: false,
|
|
32
|
+
destructiveHint: false,
|
|
33
|
+
idempotentHint: false,
|
|
34
|
+
openWorldHint: true,
|
|
35
|
+
},
|
|
29
36
|
description: 'Create an approve-gated campaign proposal from recent engagement signals and project Brand Brain.',
|
|
30
37
|
inputSchema: {
|
|
31
38
|
source_type: z.enum(['engagement_signal', 'segment_trend', 'cross_channel_synthesis']).optional().default('engagement_signal'),
|
|
@@ -72,6 +79,13 @@ export function registerDataProposalTools(server) {
|
|
|
72
79
|
return j({ proposal: data });
|
|
73
80
|
});
|
|
74
81
|
server.registerTool('list_data_proposals', {
|
|
82
|
+
title: 'List Data Proposals',
|
|
83
|
+
annotations: {
|
|
84
|
+
readOnlyHint: true,
|
|
85
|
+
destructiveHint: false,
|
|
86
|
+
idempotentHint: true,
|
|
87
|
+
openWorldHint: false,
|
|
88
|
+
},
|
|
75
89
|
description: 'List data-triggered campaign proposals for the current project.',
|
|
76
90
|
inputSchema: {
|
|
77
91
|
status: z.enum(['pending', 'approved', 'rejected', 'converted_to_draft', 'expired']).optional(),
|
|
@@ -92,6 +106,13 @@ export function registerDataProposalTools(server) {
|
|
|
92
106
|
return j({ proposals: data ?? [] });
|
|
93
107
|
});
|
|
94
108
|
server.registerTool('approve_data_proposal', {
|
|
109
|
+
title: 'Approve Data Proposal',
|
|
110
|
+
annotations: {
|
|
111
|
+
readOnlyHint: false,
|
|
112
|
+
destructiveHint: false,
|
|
113
|
+
idempotentHint: false,
|
|
114
|
+
openWorldHint: false,
|
|
115
|
+
},
|
|
95
116
|
description: 'Approve a data-triggered campaign proposal and convert it into an AI draft for review.',
|
|
96
117
|
inputSchema: {
|
|
97
118
|
proposal_id: z.string().uuid(),
|
package/build/tools/data.js
CHANGED
|
@@ -30,6 +30,13 @@ function toCsv(rows, columns) {
|
|
|
30
30
|
function toJsonl(rows) {
|
|
31
31
|
return rows.map((row) => JSON.stringify(row)).join('\n');
|
|
32
32
|
}
|
|
33
|
+
function importStatus(row) {
|
|
34
|
+
if (row.dry_run === true)
|
|
35
|
+
return 'dry_run';
|
|
36
|
+
if ((row.invalid ?? 0) > 0)
|
|
37
|
+
return 'partial';
|
|
38
|
+
return 'completed';
|
|
39
|
+
}
|
|
33
40
|
const MIGRATION_PROVIDERS = ['customerio', 'mailchimp', 'klaviyo', 'activecampaign', 'convertkit', 'kit', 'resend'];
|
|
34
41
|
const CONNECTED_PLATFORMS = [
|
|
35
42
|
'stripe',
|
|
@@ -131,14 +138,23 @@ function createCampaignMigrationTotals(options) {
|
|
|
131
138
|
},
|
|
132
139
|
};
|
|
133
140
|
}
|
|
134
|
-
function createPlatformMigrationTotals(artifactTypes) {
|
|
141
|
+
function createPlatformMigrationTotals(artifactTypes, platform) {
|
|
135
142
|
const requested = artifactTypes?.length ? new Set(artifactTypes) : null;
|
|
136
|
-
const
|
|
143
|
+
const customerIoOnly = new Set(['newsletters', 'broadcasts', 'transactional', 'snippets', 'collections', 'messages']);
|
|
144
|
+
const enabled = (key) => (!customerIoOnly.has(key) || platform === 'customerio') && (!requested || requested.has(key));
|
|
137
145
|
return {
|
|
138
146
|
contacts: { total: enabled('contacts') ? 1 : 0, processed: 0 },
|
|
139
147
|
templates: { total: enabled('templates') ? 1 : 0, processed: 0 },
|
|
140
148
|
campaigns: { total: enabled('campaigns') ? 1 : 0, processed: 0 },
|
|
141
149
|
suppressions: { total: enabled('suppressions') ? 1 : 0, processed: 0 },
|
|
150
|
+
segments: { total: enabled('segments') ? 1 : 0, processed: 0 },
|
|
151
|
+
enrollments: { total: enabled('enrollments') ? 1 : 0, processed: 0 },
|
|
152
|
+
newsletters: { total: enabled('newsletters') ? 1 : 0, processed: 0 },
|
|
153
|
+
broadcasts: { total: enabled('broadcasts') ? 1 : 0, processed: 0 },
|
|
154
|
+
transactional: { total: enabled('transactional') ? 1 : 0, processed: 0 },
|
|
155
|
+
snippets: { total: enabled('snippets') ? 1 : 0, processed: 0 },
|
|
156
|
+
collections: { total: enabled('collections') ? 1 : 0, processed: 0 },
|
|
157
|
+
messages: { total: enabled('messages') ? 1 : 0, processed: 0 },
|
|
142
158
|
processed: 0,
|
|
143
159
|
errors: 0,
|
|
144
160
|
account_name: 'Platform migration',
|
|
@@ -147,6 +163,14 @@ function createPlatformMigrationTotals(artifactTypes) {
|
|
|
147
163
|
import_templates: enabled('templates'),
|
|
148
164
|
import_campaigns: enabled('campaigns'),
|
|
149
165
|
import_suppressions: enabled('suppressions'),
|
|
166
|
+
import_segments: enabled('segments'),
|
|
167
|
+
import_enrollments: enabled('enrollments'),
|
|
168
|
+
import_newsletters: enabled('newsletters'),
|
|
169
|
+
import_broadcasts: enabled('broadcasts'),
|
|
170
|
+
import_transactional: enabled('transactional'),
|
|
171
|
+
import_snippets: enabled('snippets'),
|
|
172
|
+
import_collections: enabled('collections'),
|
|
173
|
+
import_messages: enabled('messages'),
|
|
150
174
|
migration_entry_point: 'mcp_connection',
|
|
151
175
|
},
|
|
152
176
|
};
|
|
@@ -201,6 +225,13 @@ function createZip(files) {
|
|
|
201
225
|
}
|
|
202
226
|
export function registerDataTools(server) {
|
|
203
227
|
server.registerTool('list_connected_platforms', {
|
|
228
|
+
title: 'List Connected Platforms',
|
|
229
|
+
annotations: {
|
|
230
|
+
readOnlyHint: true,
|
|
231
|
+
destructiveHint: false,
|
|
232
|
+
idempotentHint: true,
|
|
233
|
+
openWorldHint: false,
|
|
234
|
+
},
|
|
204
235
|
description: 'List connected external platforms and integration status for the current project organization. Does not return credential material.',
|
|
205
236
|
inputSchema: {
|
|
206
237
|
org_id: z.string().uuid().optional(),
|
|
@@ -232,13 +263,20 @@ export function registerDataTools(server) {
|
|
|
232
263
|
}
|
|
233
264
|
});
|
|
234
265
|
server.registerTool('connect_platform', {
|
|
266
|
+
title: 'Connect Platform',
|
|
267
|
+
annotations: {
|
|
268
|
+
readOnlyHint: false,
|
|
269
|
+
destructiveHint: false,
|
|
270
|
+
idempotentHint: false,
|
|
271
|
+
openWorldHint: true,
|
|
272
|
+
},
|
|
235
273
|
description: 'Store encrypted credentials for an external platform connection and mark it ready for trigger configuration.',
|
|
236
274
|
inputSchema: {
|
|
237
275
|
org_id: z.string().uuid().optional(),
|
|
238
276
|
platform: z.enum(CONNECTED_PLATFORMS),
|
|
239
|
-
credentials: z.record(z.unknown()),
|
|
277
|
+
credentials: z.record(z.string(), z.unknown()),
|
|
240
278
|
display_name: z.string().min(1).optional(),
|
|
241
|
-
config: z.record(z.unknown()).optional().default({}),
|
|
279
|
+
config: z.record(z.string(), z.unknown()).optional().default({}),
|
|
242
280
|
status: z.enum(PLATFORM_STATUSES).optional().default('connected'),
|
|
243
281
|
},
|
|
244
282
|
}, async ({ org_id, platform, credentials, display_name, config = {}, status = 'connected' }) => {
|
|
@@ -291,6 +329,13 @@ export function registerDataTools(server) {
|
|
|
291
329
|
}
|
|
292
330
|
});
|
|
293
331
|
server.registerTool('disconnect_platform', {
|
|
332
|
+
title: 'Disconnect Platform',
|
|
333
|
+
annotations: {
|
|
334
|
+
readOnlyHint: false,
|
|
335
|
+
destructiveHint: true,
|
|
336
|
+
idempotentHint: false,
|
|
337
|
+
openWorldHint: false,
|
|
338
|
+
},
|
|
294
339
|
description: 'Disconnect an external platform connection for the current project organization and remove stored credentials.',
|
|
295
340
|
inputSchema: {
|
|
296
341
|
org_id: z.string().uuid().optional(),
|
|
@@ -329,6 +374,13 @@ export function registerDataTools(server) {
|
|
|
329
374
|
}
|
|
330
375
|
});
|
|
331
376
|
server.registerTool('list_platform_events', {
|
|
377
|
+
title: 'List Platform Events',
|
|
378
|
+
annotations: {
|
|
379
|
+
readOnlyHint: true,
|
|
380
|
+
destructiveHint: false,
|
|
381
|
+
idempotentHint: true,
|
|
382
|
+
openWorldHint: false,
|
|
383
|
+
},
|
|
332
384
|
description: 'List recent raw events received from connected platforms for troubleshooting trigger mappings.',
|
|
333
385
|
inputSchema: {
|
|
334
386
|
org_id: z.string().uuid().optional(),
|
|
@@ -359,6 +411,13 @@ export function registerDataTools(server) {
|
|
|
359
411
|
}
|
|
360
412
|
});
|
|
361
413
|
server.registerTool('configure_platform_trigger', {
|
|
414
|
+
title: 'Configure Platform Trigger',
|
|
415
|
+
annotations: {
|
|
416
|
+
readOnlyHint: false,
|
|
417
|
+
destructiveHint: false,
|
|
418
|
+
idempotentHint: false,
|
|
419
|
+
openWorldHint: false,
|
|
420
|
+
},
|
|
362
421
|
description: 'Configure an event-to-action mapping for a connected platform, such as adding a Shopify customer to a list after an order.',
|
|
363
422
|
inputSchema: {
|
|
364
423
|
org_id: z.string().uuid().optional(),
|
|
@@ -422,19 +481,40 @@ export function registerDataTools(server) {
|
|
|
422
481
|
}
|
|
423
482
|
});
|
|
424
483
|
server.registerTool('list_import_history', {
|
|
484
|
+
title: 'List Import History',
|
|
485
|
+
annotations: {
|
|
486
|
+
readOnlyHint: true,
|
|
487
|
+
destructiveHint: false,
|
|
488
|
+
idempotentHint: true,
|
|
489
|
+
openWorldHint: false,
|
|
490
|
+
},
|
|
425
491
|
description: 'List recent subscriber import history rows.',
|
|
426
492
|
inputSchema: { limit: z.number().int().min(1).max(100).optional().default(25) },
|
|
427
493
|
}, async ({ limit = 25 }) => {
|
|
428
494
|
const { data, error } = await db().from('import_log')
|
|
429
|
-
.select('id, source,
|
|
495
|
+
.select('id, source, imported, skipped, invalid, total_found, dry_run, tags, created_at')
|
|
430
496
|
.eq('project_id', getProjectId())
|
|
431
497
|
.order('created_at', { ascending: false })
|
|
432
498
|
.limit(limit);
|
|
433
499
|
if (error)
|
|
434
500
|
return j({ error: error.message });
|
|
435
|
-
return j({
|
|
501
|
+
return j({
|
|
502
|
+
imports: (data ?? []).map((row) => ({
|
|
503
|
+
...row,
|
|
504
|
+
status: importStatus(row),
|
|
505
|
+
imported_count: row.imported,
|
|
506
|
+
failed_count: row.invalid,
|
|
507
|
+
})),
|
|
508
|
+
});
|
|
436
509
|
});
|
|
437
510
|
server.registerTool('list_api_keys', {
|
|
511
|
+
title: 'List API Keys',
|
|
512
|
+
annotations: {
|
|
513
|
+
readOnlyHint: true,
|
|
514
|
+
destructiveHint: false,
|
|
515
|
+
idempotentHint: true,
|
|
516
|
+
openWorldHint: false,
|
|
517
|
+
},
|
|
438
518
|
description: 'List API keys without returning secret material.',
|
|
439
519
|
inputSchema: {},
|
|
440
520
|
}, async () => {
|
|
@@ -448,6 +528,13 @@ export function registerDataTools(server) {
|
|
|
448
528
|
return j({ keys: data ?? [] });
|
|
449
529
|
});
|
|
450
530
|
server.registerTool('create_api_key', {
|
|
531
|
+
title: 'Create API Key',
|
|
532
|
+
annotations: {
|
|
533
|
+
readOnlyHint: false,
|
|
534
|
+
destructiveHint: false,
|
|
535
|
+
idempotentHint: false,
|
|
536
|
+
openWorldHint: false,
|
|
537
|
+
},
|
|
451
538
|
description: 'Create an API key and return the secret once.',
|
|
452
539
|
inputSchema: {
|
|
453
540
|
name: z.string().min(1),
|
|
@@ -466,6 +553,13 @@ export function registerDataTools(server) {
|
|
|
466
553
|
return j({ key_id: data.id, name: data.name, slug: data.slug, secret });
|
|
467
554
|
});
|
|
468
555
|
server.registerTool('revoke_api_key', {
|
|
556
|
+
title: 'Revoke API Key',
|
|
557
|
+
annotations: {
|
|
558
|
+
readOnlyHint: false,
|
|
559
|
+
destructiveHint: true,
|
|
560
|
+
idempotentHint: false,
|
|
561
|
+
openWorldHint: false,
|
|
562
|
+
},
|
|
469
563
|
description: 'Revoke an API key.',
|
|
470
564
|
inputSchema: { key_id: z.string().uuid() },
|
|
471
565
|
}, async ({ key_id }) => {
|
|
@@ -475,6 +569,13 @@ export function registerDataTools(server) {
|
|
|
475
569
|
return j({ revoked: true });
|
|
476
570
|
});
|
|
477
571
|
server.registerTool('migrate_campaigns_from', {
|
|
572
|
+
title: 'Migrate Campaigns From',
|
|
573
|
+
annotations: {
|
|
574
|
+
readOnlyHint: false,
|
|
575
|
+
destructiveHint: false,
|
|
576
|
+
idempotentHint: false,
|
|
577
|
+
openWorldHint: true,
|
|
578
|
+
},
|
|
478
579
|
description: 'Start a campaign-artifact migration from a provider token. Imports templates and campaigns as draft, approval-required Sendinel assets; Customer.io is the first-class path.',
|
|
479
580
|
inputSchema: {
|
|
480
581
|
provider: z.enum(MIGRATION_PROVIDERS),
|
|
@@ -539,10 +640,20 @@ export function registerDataTools(server) {
|
|
|
539
640
|
});
|
|
540
641
|
});
|
|
541
642
|
server.registerTool('start_platform_migration', {
|
|
643
|
+
title: 'Start Platform Migration',
|
|
644
|
+
annotations: {
|
|
645
|
+
readOnlyHint: false,
|
|
646
|
+
destructiveHint: false,
|
|
647
|
+
idempotentHint: false,
|
|
648
|
+
openWorldHint: true,
|
|
649
|
+
},
|
|
542
650
|
description: 'Start a migration from a stored external platform connection into Sendinel. Returns a job_id for status polling.',
|
|
543
651
|
inputSchema: {
|
|
544
652
|
connection_id: z.string().uuid(),
|
|
545
|
-
artifact_types: z.array(z.enum([
|
|
653
|
+
artifact_types: z.array(z.enum([
|
|
654
|
+
'contacts', 'templates', 'campaigns', 'suppressions', 'segments', 'enrollments',
|
|
655
|
+
'newsletters', 'broadcasts', 'transactional', 'snippets', 'collections', 'messages',
|
|
656
|
+
])).optional(),
|
|
546
657
|
dry_run: z.boolean().optional().default(false),
|
|
547
658
|
},
|
|
548
659
|
}, async ({ connection_id, artifact_types, dry_run = false }) => {
|
|
@@ -574,7 +685,7 @@ export function registerDataTools(server) {
|
|
|
574
685
|
provider: connection.platform,
|
|
575
686
|
status: 'pending',
|
|
576
687
|
progress_pct: 0,
|
|
577
|
-
totals: createPlatformMigrationTotals(artifact_types),
|
|
688
|
+
totals: createPlatformMigrationTotals(artifact_types, connection.platform),
|
|
578
689
|
errors: [],
|
|
579
690
|
credentials_enc: encrypt(JSON.stringify(credentials)),
|
|
580
691
|
field_mapping: null,
|
|
@@ -588,6 +699,13 @@ export function registerDataTools(server) {
|
|
|
588
699
|
return j({ job_id: data.id, status: 'pending', connection_id, provider: connection.platform });
|
|
589
700
|
});
|
|
590
701
|
server.registerTool('list_notifications', {
|
|
702
|
+
title: 'List Notifications',
|
|
703
|
+
annotations: {
|
|
704
|
+
readOnlyHint: true,
|
|
705
|
+
destructiveHint: false,
|
|
706
|
+
idempotentHint: true,
|
|
707
|
+
openWorldHint: false,
|
|
708
|
+
},
|
|
591
709
|
description: 'List in-app notifications.',
|
|
592
710
|
inputSchema: { unread_only: z.boolean().optional().default(false) },
|
|
593
711
|
}, async ({ unread_only = false }) => {
|
|
@@ -600,6 +718,13 @@ export function registerDataTools(server) {
|
|
|
600
718
|
return j({ notifications: data ?? [] });
|
|
601
719
|
});
|
|
602
720
|
server.registerTool('mark_notifications_read', {
|
|
721
|
+
title: 'Mark Notifications Read',
|
|
722
|
+
annotations: {
|
|
723
|
+
readOnlyHint: false,
|
|
724
|
+
destructiveHint: false,
|
|
725
|
+
idempotentHint: true,
|
|
726
|
+
openWorldHint: false,
|
|
727
|
+
},
|
|
603
728
|
description: 'Mark selected notifications read, or all unread notifications if ids is omitted.',
|
|
604
729
|
inputSchema: { ids: z.array(z.string().uuid()).optional() },
|
|
605
730
|
}, async ({ ids }) => {
|
|
@@ -612,6 +737,13 @@ export function registerDataTools(server) {
|
|
|
612
737
|
return j({ updated: data?.length ?? 0 });
|
|
613
738
|
});
|
|
614
739
|
server.registerTool('tenant_export_full', {
|
|
740
|
+
title: 'Tenant Export Full',
|
|
741
|
+
annotations: {
|
|
742
|
+
readOnlyHint: true,
|
|
743
|
+
destructiveHint: false,
|
|
744
|
+
idempotentHint: true,
|
|
745
|
+
openWorldHint: false,
|
|
746
|
+
},
|
|
615
747
|
description: 'Export all tenant data classes for portability. Returns inline data for small exports or a signed archive URL for large exports.',
|
|
616
748
|
inputSchema: {
|
|
617
749
|
siteId: z.string().uuid(),
|