@sendinel/mcp-server 1.0.4 → 1.0.5
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 +121 -5
- 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',
|
|
@@ -139,6 +146,8 @@ function createPlatformMigrationTotals(artifactTypes) {
|
|
|
139
146
|
templates: { total: enabled('templates') ? 1 : 0, processed: 0 },
|
|
140
147
|
campaigns: { total: enabled('campaigns') ? 1 : 0, processed: 0 },
|
|
141
148
|
suppressions: { total: enabled('suppressions') ? 1 : 0, processed: 0 },
|
|
149
|
+
segments: { total: enabled('segments') ? 1 : 0, processed: 0 },
|
|
150
|
+
enrollments: { total: enabled('enrollments') ? 1 : 0, processed: 0 },
|
|
142
151
|
processed: 0,
|
|
143
152
|
errors: 0,
|
|
144
153
|
account_name: 'Platform migration',
|
|
@@ -147,6 +156,8 @@ function createPlatformMigrationTotals(artifactTypes) {
|
|
|
147
156
|
import_templates: enabled('templates'),
|
|
148
157
|
import_campaigns: enabled('campaigns'),
|
|
149
158
|
import_suppressions: enabled('suppressions'),
|
|
159
|
+
import_segments: enabled('segments'),
|
|
160
|
+
import_enrollments: enabled('enrollments'),
|
|
150
161
|
migration_entry_point: 'mcp_connection',
|
|
151
162
|
},
|
|
152
163
|
};
|
|
@@ -201,6 +212,13 @@ function createZip(files) {
|
|
|
201
212
|
}
|
|
202
213
|
export function registerDataTools(server) {
|
|
203
214
|
server.registerTool('list_connected_platforms', {
|
|
215
|
+
title: 'List Connected Platforms',
|
|
216
|
+
annotations: {
|
|
217
|
+
readOnlyHint: true,
|
|
218
|
+
destructiveHint: false,
|
|
219
|
+
idempotentHint: true,
|
|
220
|
+
openWorldHint: false,
|
|
221
|
+
},
|
|
204
222
|
description: 'List connected external platforms and integration status for the current project organization. Does not return credential material.',
|
|
205
223
|
inputSchema: {
|
|
206
224
|
org_id: z.string().uuid().optional(),
|
|
@@ -232,13 +250,20 @@ export function registerDataTools(server) {
|
|
|
232
250
|
}
|
|
233
251
|
});
|
|
234
252
|
server.registerTool('connect_platform', {
|
|
253
|
+
title: 'Connect Platform',
|
|
254
|
+
annotations: {
|
|
255
|
+
readOnlyHint: false,
|
|
256
|
+
destructiveHint: false,
|
|
257
|
+
idempotentHint: false,
|
|
258
|
+
openWorldHint: true,
|
|
259
|
+
},
|
|
235
260
|
description: 'Store encrypted credentials for an external platform connection and mark it ready for trigger configuration.',
|
|
236
261
|
inputSchema: {
|
|
237
262
|
org_id: z.string().uuid().optional(),
|
|
238
263
|
platform: z.enum(CONNECTED_PLATFORMS),
|
|
239
|
-
credentials: z.record(z.unknown()),
|
|
264
|
+
credentials: z.record(z.string(), z.unknown()),
|
|
240
265
|
display_name: z.string().min(1).optional(),
|
|
241
|
-
config: z.record(z.unknown()).optional().default({}),
|
|
266
|
+
config: z.record(z.string(), z.unknown()).optional().default({}),
|
|
242
267
|
status: z.enum(PLATFORM_STATUSES).optional().default('connected'),
|
|
243
268
|
},
|
|
244
269
|
}, async ({ org_id, platform, credentials, display_name, config = {}, status = 'connected' }) => {
|
|
@@ -291,6 +316,13 @@ export function registerDataTools(server) {
|
|
|
291
316
|
}
|
|
292
317
|
});
|
|
293
318
|
server.registerTool('disconnect_platform', {
|
|
319
|
+
title: 'Disconnect Platform',
|
|
320
|
+
annotations: {
|
|
321
|
+
readOnlyHint: false,
|
|
322
|
+
destructiveHint: true,
|
|
323
|
+
idempotentHint: false,
|
|
324
|
+
openWorldHint: false,
|
|
325
|
+
},
|
|
294
326
|
description: 'Disconnect an external platform connection for the current project organization and remove stored credentials.',
|
|
295
327
|
inputSchema: {
|
|
296
328
|
org_id: z.string().uuid().optional(),
|
|
@@ -329,6 +361,13 @@ export function registerDataTools(server) {
|
|
|
329
361
|
}
|
|
330
362
|
});
|
|
331
363
|
server.registerTool('list_platform_events', {
|
|
364
|
+
title: 'List Platform Events',
|
|
365
|
+
annotations: {
|
|
366
|
+
readOnlyHint: true,
|
|
367
|
+
destructiveHint: false,
|
|
368
|
+
idempotentHint: true,
|
|
369
|
+
openWorldHint: false,
|
|
370
|
+
},
|
|
332
371
|
description: 'List recent raw events received from connected platforms for troubleshooting trigger mappings.',
|
|
333
372
|
inputSchema: {
|
|
334
373
|
org_id: z.string().uuid().optional(),
|
|
@@ -359,6 +398,13 @@ export function registerDataTools(server) {
|
|
|
359
398
|
}
|
|
360
399
|
});
|
|
361
400
|
server.registerTool('configure_platform_trigger', {
|
|
401
|
+
title: 'Configure Platform Trigger',
|
|
402
|
+
annotations: {
|
|
403
|
+
readOnlyHint: false,
|
|
404
|
+
destructiveHint: false,
|
|
405
|
+
idempotentHint: false,
|
|
406
|
+
openWorldHint: false,
|
|
407
|
+
},
|
|
362
408
|
description: 'Configure an event-to-action mapping for a connected platform, such as adding a Shopify customer to a list after an order.',
|
|
363
409
|
inputSchema: {
|
|
364
410
|
org_id: z.string().uuid().optional(),
|
|
@@ -422,19 +468,40 @@ export function registerDataTools(server) {
|
|
|
422
468
|
}
|
|
423
469
|
});
|
|
424
470
|
server.registerTool('list_import_history', {
|
|
471
|
+
title: 'List Import History',
|
|
472
|
+
annotations: {
|
|
473
|
+
readOnlyHint: true,
|
|
474
|
+
destructiveHint: false,
|
|
475
|
+
idempotentHint: true,
|
|
476
|
+
openWorldHint: false,
|
|
477
|
+
},
|
|
425
478
|
description: 'List recent subscriber import history rows.',
|
|
426
479
|
inputSchema: { limit: z.number().int().min(1).max(100).optional().default(25) },
|
|
427
480
|
}, async ({ limit = 25 }) => {
|
|
428
481
|
const { data, error } = await db().from('import_log')
|
|
429
|
-
.select('id, source,
|
|
482
|
+
.select('id, source, imported, skipped, invalid, total_found, dry_run, tags, created_at')
|
|
430
483
|
.eq('project_id', getProjectId())
|
|
431
484
|
.order('created_at', { ascending: false })
|
|
432
485
|
.limit(limit);
|
|
433
486
|
if (error)
|
|
434
487
|
return j({ error: error.message });
|
|
435
|
-
return j({
|
|
488
|
+
return j({
|
|
489
|
+
imports: (data ?? []).map((row) => ({
|
|
490
|
+
...row,
|
|
491
|
+
status: importStatus(row),
|
|
492
|
+
imported_count: row.imported,
|
|
493
|
+
failed_count: row.invalid,
|
|
494
|
+
})),
|
|
495
|
+
});
|
|
436
496
|
});
|
|
437
497
|
server.registerTool('list_api_keys', {
|
|
498
|
+
title: 'List API Keys',
|
|
499
|
+
annotations: {
|
|
500
|
+
readOnlyHint: true,
|
|
501
|
+
destructiveHint: false,
|
|
502
|
+
idempotentHint: true,
|
|
503
|
+
openWorldHint: false,
|
|
504
|
+
},
|
|
438
505
|
description: 'List API keys without returning secret material.',
|
|
439
506
|
inputSchema: {},
|
|
440
507
|
}, async () => {
|
|
@@ -448,6 +515,13 @@ export function registerDataTools(server) {
|
|
|
448
515
|
return j({ keys: data ?? [] });
|
|
449
516
|
});
|
|
450
517
|
server.registerTool('create_api_key', {
|
|
518
|
+
title: 'Create API Key',
|
|
519
|
+
annotations: {
|
|
520
|
+
readOnlyHint: false,
|
|
521
|
+
destructiveHint: false,
|
|
522
|
+
idempotentHint: false,
|
|
523
|
+
openWorldHint: false,
|
|
524
|
+
},
|
|
451
525
|
description: 'Create an API key and return the secret once.',
|
|
452
526
|
inputSchema: {
|
|
453
527
|
name: z.string().min(1),
|
|
@@ -466,6 +540,13 @@ export function registerDataTools(server) {
|
|
|
466
540
|
return j({ key_id: data.id, name: data.name, slug: data.slug, secret });
|
|
467
541
|
});
|
|
468
542
|
server.registerTool('revoke_api_key', {
|
|
543
|
+
title: 'Revoke API Key',
|
|
544
|
+
annotations: {
|
|
545
|
+
readOnlyHint: false,
|
|
546
|
+
destructiveHint: true,
|
|
547
|
+
idempotentHint: false,
|
|
548
|
+
openWorldHint: false,
|
|
549
|
+
},
|
|
469
550
|
description: 'Revoke an API key.',
|
|
470
551
|
inputSchema: { key_id: z.string().uuid() },
|
|
471
552
|
}, async ({ key_id }) => {
|
|
@@ -475,6 +556,13 @@ export function registerDataTools(server) {
|
|
|
475
556
|
return j({ revoked: true });
|
|
476
557
|
});
|
|
477
558
|
server.registerTool('migrate_campaigns_from', {
|
|
559
|
+
title: 'Migrate Campaigns From',
|
|
560
|
+
annotations: {
|
|
561
|
+
readOnlyHint: false,
|
|
562
|
+
destructiveHint: false,
|
|
563
|
+
idempotentHint: false,
|
|
564
|
+
openWorldHint: true,
|
|
565
|
+
},
|
|
478
566
|
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
567
|
inputSchema: {
|
|
480
568
|
provider: z.enum(MIGRATION_PROVIDERS),
|
|
@@ -539,10 +627,17 @@ export function registerDataTools(server) {
|
|
|
539
627
|
});
|
|
540
628
|
});
|
|
541
629
|
server.registerTool('start_platform_migration', {
|
|
630
|
+
title: 'Start Platform Migration',
|
|
631
|
+
annotations: {
|
|
632
|
+
readOnlyHint: false,
|
|
633
|
+
destructiveHint: false,
|
|
634
|
+
idempotentHint: false,
|
|
635
|
+
openWorldHint: true,
|
|
636
|
+
},
|
|
542
637
|
description: 'Start a migration from a stored external platform connection into Sendinel. Returns a job_id for status polling.',
|
|
543
638
|
inputSchema: {
|
|
544
639
|
connection_id: z.string().uuid(),
|
|
545
|
-
artifact_types: z.array(z.enum(['contacts', 'templates', 'campaigns', 'suppressions'])).optional(),
|
|
640
|
+
artifact_types: z.array(z.enum(['contacts', 'templates', 'campaigns', 'suppressions', 'segments', 'enrollments'])).optional(),
|
|
546
641
|
dry_run: z.boolean().optional().default(false),
|
|
547
642
|
},
|
|
548
643
|
}, async ({ connection_id, artifact_types, dry_run = false }) => {
|
|
@@ -588,6 +683,13 @@ export function registerDataTools(server) {
|
|
|
588
683
|
return j({ job_id: data.id, status: 'pending', connection_id, provider: connection.platform });
|
|
589
684
|
});
|
|
590
685
|
server.registerTool('list_notifications', {
|
|
686
|
+
title: 'List Notifications',
|
|
687
|
+
annotations: {
|
|
688
|
+
readOnlyHint: true,
|
|
689
|
+
destructiveHint: false,
|
|
690
|
+
idempotentHint: true,
|
|
691
|
+
openWorldHint: false,
|
|
692
|
+
},
|
|
591
693
|
description: 'List in-app notifications.',
|
|
592
694
|
inputSchema: { unread_only: z.boolean().optional().default(false) },
|
|
593
695
|
}, async ({ unread_only = false }) => {
|
|
@@ -600,6 +702,13 @@ export function registerDataTools(server) {
|
|
|
600
702
|
return j({ notifications: data ?? [] });
|
|
601
703
|
});
|
|
602
704
|
server.registerTool('mark_notifications_read', {
|
|
705
|
+
title: 'Mark Notifications Read',
|
|
706
|
+
annotations: {
|
|
707
|
+
readOnlyHint: false,
|
|
708
|
+
destructiveHint: false,
|
|
709
|
+
idempotentHint: true,
|
|
710
|
+
openWorldHint: false,
|
|
711
|
+
},
|
|
603
712
|
description: 'Mark selected notifications read, or all unread notifications if ids is omitted.',
|
|
604
713
|
inputSchema: { ids: z.array(z.string().uuid()).optional() },
|
|
605
714
|
}, async ({ ids }) => {
|
|
@@ -612,6 +721,13 @@ export function registerDataTools(server) {
|
|
|
612
721
|
return j({ updated: data?.length ?? 0 });
|
|
613
722
|
});
|
|
614
723
|
server.registerTool('tenant_export_full', {
|
|
724
|
+
title: 'Tenant Export Full',
|
|
725
|
+
annotations: {
|
|
726
|
+
readOnlyHint: true,
|
|
727
|
+
destructiveHint: false,
|
|
728
|
+
idempotentHint: true,
|
|
729
|
+
openWorldHint: false,
|
|
730
|
+
},
|
|
615
731
|
description: 'Export all tenant data classes for portability. Returns inline data for small exports or a signed archive URL for large exports.',
|
|
616
732
|
inputSchema: {
|
|
617
733
|
siteId: z.string().uuid(),
|