@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
|
@@ -4,6 +4,13 @@ import { getProjectId } from '../project.js';
|
|
|
4
4
|
const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
5
5
|
export function registerDeliveryOpsTools(server) {
|
|
6
6
|
server.registerTool('refresh_domain_dns', {
|
|
7
|
+
title: 'Refresh Domain DNS',
|
|
8
|
+
annotations: {
|
|
9
|
+
readOnlyHint: true,
|
|
10
|
+
destructiveHint: false,
|
|
11
|
+
idempotentHint: true,
|
|
12
|
+
openWorldHint: true,
|
|
13
|
+
},
|
|
7
14
|
description: 'Refresh and return stored DNS verification status for a sending domain.',
|
|
8
15
|
inputSchema: { domain_id: z.string().uuid() },
|
|
9
16
|
}, async ({ domain_id }) => {
|
|
@@ -23,6 +30,13 @@ export function registerDeliveryOpsTools(server) {
|
|
|
23
30
|
return j({ spf, dkim, dmarc, all_verified: Boolean(spf && dkim && dmarc) });
|
|
24
31
|
});
|
|
25
32
|
server.registerTool('dnsbl_check', {
|
|
33
|
+
title: 'DNSBL Check',
|
|
34
|
+
annotations: {
|
|
35
|
+
readOnlyHint: true,
|
|
36
|
+
destructiveHint: false,
|
|
37
|
+
idempotentHint: true,
|
|
38
|
+
openWorldHint: true,
|
|
39
|
+
},
|
|
26
40
|
description: 'Return the latest DNSBL snapshot for all monitored zones for a site sending domain.',
|
|
27
41
|
inputSchema: {
|
|
28
42
|
siteId: z.string().uuid(),
|
|
@@ -58,6 +72,13 @@ export function registerDeliveryOpsTools(server) {
|
|
|
58
72
|
});
|
|
59
73
|
});
|
|
60
74
|
server.registerTool('dnsbl_delisting_draft', {
|
|
75
|
+
title: 'DNSBL Delisting Draft',
|
|
76
|
+
annotations: {
|
|
77
|
+
readOnlyHint: true,
|
|
78
|
+
destructiveHint: false,
|
|
79
|
+
idempotentHint: true,
|
|
80
|
+
openWorldHint: true,
|
|
81
|
+
},
|
|
61
82
|
description: 'Draft a DNSBL delisting ticket using tenant send metrics as evidence. Does not auto-submit.',
|
|
62
83
|
inputSchema: {
|
|
63
84
|
siteId: z.string().uuid(),
|
|
@@ -113,6 +134,13 @@ export function registerDeliveryOpsTools(server) {
|
|
|
113
134
|
});
|
|
114
135
|
});
|
|
115
136
|
server.registerTool('get_cron_runs', {
|
|
137
|
+
title: 'Get Cron Runs',
|
|
138
|
+
annotations: {
|
|
139
|
+
readOnlyHint: true,
|
|
140
|
+
destructiveHint: false,
|
|
141
|
+
idempotentHint: true,
|
|
142
|
+
openWorldHint: false,
|
|
143
|
+
},
|
|
116
144
|
description: 'List recent cron job executions to diagnose why scheduled sends are not firing.',
|
|
117
145
|
inputSchema: {
|
|
118
146
|
job_name: z.string().optional(),
|
|
@@ -136,6 +164,13 @@ export function registerDeliveryOpsTools(server) {
|
|
|
136
164
|
return j({ runs: data ?? [], recent_failures: failed.length, total: data?.length ?? 0 });
|
|
137
165
|
});
|
|
138
166
|
server.registerTool('get_queue_status', {
|
|
167
|
+
title: 'Get Queue Status',
|
|
168
|
+
annotations: {
|
|
169
|
+
readOnlyHint: true,
|
|
170
|
+
destructiveHint: false,
|
|
171
|
+
idempotentHint: true,
|
|
172
|
+
openWorldHint: false,
|
|
173
|
+
},
|
|
139
174
|
description: 'Get the current send queue status — pending, processing, retry, and failed counts.',
|
|
140
175
|
inputSchema: {
|
|
141
176
|
site_id: z.string().uuid().optional(),
|
|
@@ -167,6 +202,13 @@ export function registerDeliveryOpsTools(server) {
|
|
|
167
202
|
});
|
|
168
203
|
});
|
|
169
204
|
server.registerTool('get_send_dlq', {
|
|
205
|
+
title: 'Get Send DLQ',
|
|
206
|
+
annotations: {
|
|
207
|
+
readOnlyHint: true,
|
|
208
|
+
destructiveHint: false,
|
|
209
|
+
idempotentHint: true,
|
|
210
|
+
openWorldHint: false,
|
|
211
|
+
},
|
|
170
212
|
description: 'List messages in the send dead-letter queue — emails that failed all retries.',
|
|
171
213
|
inputSchema: {
|
|
172
214
|
limit: z.number().int().min(1).max(50).default(20).optional(),
|
|
@@ -187,6 +229,13 @@ export function registerDeliveryOpsTools(server) {
|
|
|
187
229
|
return j({ dlq: data ?? [], count: data?.length ?? 0 });
|
|
188
230
|
});
|
|
189
231
|
server.registerTool('get_scoring_rules', {
|
|
232
|
+
title: 'Get Scoring Rules',
|
|
233
|
+
annotations: {
|
|
234
|
+
readOnlyHint: true,
|
|
235
|
+
destructiveHint: false,
|
|
236
|
+
idempotentHint: true,
|
|
237
|
+
openWorldHint: false,
|
|
238
|
+
},
|
|
190
239
|
description: 'Get contact scoring rules for the current project.',
|
|
191
240
|
inputSchema: {},
|
|
192
241
|
}, async () => {
|
|
@@ -200,6 +249,13 @@ export function registerDeliveryOpsTools(server) {
|
|
|
200
249
|
return j({ rules: data });
|
|
201
250
|
});
|
|
202
251
|
server.registerTool('update_scoring_rules', {
|
|
252
|
+
title: 'Update Scoring Rules',
|
|
253
|
+
annotations: {
|
|
254
|
+
readOnlyHint: false,
|
|
255
|
+
destructiveHint: false,
|
|
256
|
+
idempotentHint: true,
|
|
257
|
+
openWorldHint: false,
|
|
258
|
+
},
|
|
203
259
|
description: 'Update contact scoring rules. Only the provided fields are changed.',
|
|
204
260
|
inputSchema: {
|
|
205
261
|
open_weight: z.number().min(0).max(100).optional(),
|
|
@@ -230,6 +286,13 @@ export function registerDeliveryOpsTools(server) {
|
|
|
230
286
|
return j({ rules: data, ok: true });
|
|
231
287
|
});
|
|
232
288
|
server.registerTool('explain_contact_score', {
|
|
289
|
+
title: 'Explain Contact Score',
|
|
290
|
+
annotations: {
|
|
291
|
+
readOnlyHint: true,
|
|
292
|
+
destructiveHint: false,
|
|
293
|
+
idempotentHint: true,
|
|
294
|
+
openWorldHint: false,
|
|
295
|
+
},
|
|
233
296
|
description: 'Explain why a subscriber has their current engagement score.',
|
|
234
297
|
inputSchema: {
|
|
235
298
|
contact_id: z.string().uuid().optional(),
|
|
@@ -299,6 +362,13 @@ export function registerDeliveryOpsTools(server) {
|
|
|
299
362
|
});
|
|
300
363
|
});
|
|
301
364
|
server.registerTool('export_data', {
|
|
365
|
+
title: 'Export Data',
|
|
366
|
+
annotations: {
|
|
367
|
+
readOnlyHint: true,
|
|
368
|
+
destructiveHint: false,
|
|
369
|
+
idempotentHint: true,
|
|
370
|
+
openWorldHint: false,
|
|
371
|
+
},
|
|
302
372
|
description: 'Export contacts, email log, campaign, or suppression data as JSON.',
|
|
303
373
|
inputSchema: {
|
|
304
374
|
resource: z.enum(['contacts', 'email_log', 'campaigns', 'suppressions']),
|
|
@@ -326,6 +396,13 @@ export function registerDeliveryOpsTools(server) {
|
|
|
326
396
|
return j({ resource, count: data?.length ?? 0, data: data ?? [], truncated: (data?.length ?? 0) === limit });
|
|
327
397
|
});
|
|
328
398
|
server.registerTool('list_domains', {
|
|
399
|
+
title: 'List Domains',
|
|
400
|
+
annotations: {
|
|
401
|
+
readOnlyHint: true,
|
|
402
|
+
destructiveHint: false,
|
|
403
|
+
idempotentHint: true,
|
|
404
|
+
openWorldHint: false,
|
|
405
|
+
},
|
|
329
406
|
description: 'List sending domains configured for the project and their current verification status.',
|
|
330
407
|
inputSchema: { site_id: z.string().uuid().optional() },
|
|
331
408
|
}, async ({ site_id }) => {
|
|
@@ -350,6 +427,13 @@ export function registerDeliveryOpsTools(server) {
|
|
|
350
427
|
return j({ domains: data ?? [], count: data?.length ?? 0 });
|
|
351
428
|
});
|
|
352
429
|
server.registerTool('register_domain', {
|
|
430
|
+
title: 'Register Domain',
|
|
431
|
+
annotations: {
|
|
432
|
+
readOnlyHint: false,
|
|
433
|
+
destructiveHint: false,
|
|
434
|
+
idempotentHint: false,
|
|
435
|
+
openWorldHint: true,
|
|
436
|
+
},
|
|
353
437
|
description: 'Register a sending domain for a site and link it to the site record.',
|
|
354
438
|
inputSchema: {
|
|
355
439
|
site_id: z.string().uuid(),
|
package/build/tools/drafts.js
CHANGED
|
@@ -2,8 +2,16 @@ import { z } from 'zod';
|
|
|
2
2
|
import { db } from '../db.js';
|
|
3
3
|
import { getProjectId } from '../project.js';
|
|
4
4
|
import { checkPlanLimit } from '../plan-limits.js';
|
|
5
|
+
import { recordBrandAssetUsage } from '../lib/brand-asset-usage.js';
|
|
5
6
|
export function registerDraftTools(server) {
|
|
6
7
|
server.registerTool('create_draft', {
|
|
8
|
+
title: 'Create Draft',
|
|
9
|
+
annotations: {
|
|
10
|
+
readOnlyHint: false,
|
|
11
|
+
destructiveHint: false,
|
|
12
|
+
idempotentHint: false,
|
|
13
|
+
openWorldHint: false,
|
|
14
|
+
},
|
|
7
15
|
description: 'Create an AI-generated email draft pending human approval',
|
|
8
16
|
inputSchema: {
|
|
9
17
|
site_id: z.string().uuid(),
|
|
@@ -13,8 +21,9 @@ export function registerDraftTools(server) {
|
|
|
13
21
|
campaign_id: z.string().uuid().optional(),
|
|
14
22
|
prompt: z.string().optional().describe('The prompt or brief used to generate this draft'),
|
|
15
23
|
model: z.string().optional().describe('Model that generated the draft, defaults to ANTHROPIC_MODEL_ID'),
|
|
24
|
+
brand_asset_ids: z.array(z.string().uuid()).optional().describe('Stable brand_media_assets ids explicitly referenced by this draft.'),
|
|
16
25
|
},
|
|
17
|
-
}, async ({ site_id, subject, body_html, body_text, campaign_id, prompt, model }) => {
|
|
26
|
+
}, async ({ site_id, subject, body_html, body_text, campaign_id, prompt, model, brand_asset_ids }) => {
|
|
18
27
|
const limit = await checkPlanLimit('ai_drafts');
|
|
19
28
|
if (!limit.allowed) {
|
|
20
29
|
return { content: [{ type: 'text', text: `AI draft limit reached (${limit.current}/${limit.limit}). Upgrade to BYOD for unlimited drafts.` }] };
|
|
@@ -26,9 +35,26 @@ export function registerDraftTools(server) {
|
|
|
26
35
|
.single();
|
|
27
36
|
if (error)
|
|
28
37
|
return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
|
|
38
|
+
await recordBrandAssetUsage({
|
|
39
|
+
client: db(),
|
|
40
|
+
projectId: getProjectId(),
|
|
41
|
+
source: 'ai_draft',
|
|
42
|
+
resourceId: data.id,
|
|
43
|
+
html: body_html,
|
|
44
|
+
assetIds: brand_asset_ids,
|
|
45
|
+
}).catch((err) => {
|
|
46
|
+
console.warn('Failed to record brand asset usage for MCP draft:', err);
|
|
47
|
+
});
|
|
29
48
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
30
49
|
});
|
|
31
50
|
server.registerTool('list_drafts', {
|
|
51
|
+
title: 'List Drafts',
|
|
52
|
+
annotations: {
|
|
53
|
+
readOnlyHint: true,
|
|
54
|
+
destructiveHint: false,
|
|
55
|
+
idempotentHint: true,
|
|
56
|
+
openWorldHint: false,
|
|
57
|
+
},
|
|
32
58
|
description: 'List AI email drafts by status',
|
|
33
59
|
inputSchema: {
|
|
34
60
|
site_id: z.string().uuid().optional(),
|
|
@@ -52,6 +78,13 @@ export function registerDraftTools(server) {
|
|
|
52
78
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
53
79
|
});
|
|
54
80
|
server.registerTool('get_draft', {
|
|
81
|
+
title: 'Get Draft',
|
|
82
|
+
annotations: {
|
|
83
|
+
readOnlyHint: true,
|
|
84
|
+
destructiveHint: false,
|
|
85
|
+
idempotentHint: true,
|
|
86
|
+
openWorldHint: false,
|
|
87
|
+
},
|
|
55
88
|
description: 'Get one AI email draft by ID, including full body content.',
|
|
56
89
|
inputSchema: {
|
|
57
90
|
draft_id: z.string().uuid(),
|
|
@@ -70,6 +103,13 @@ export function registerDraftTools(server) {
|
|
|
70
103
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
71
104
|
});
|
|
72
105
|
server.registerTool('promote_draft_to_campaign', {
|
|
106
|
+
title: 'Promote Draft To Campaign',
|
|
107
|
+
annotations: {
|
|
108
|
+
readOnlyHint: false,
|
|
109
|
+
destructiveHint: false,
|
|
110
|
+
idempotentHint: false,
|
|
111
|
+
openWorldHint: false,
|
|
112
|
+
},
|
|
73
113
|
description: 'Promote an AI draft into an email campaign in one call. Creates the campaign and first campaign step, then links the draft to the campaign. The campaign is created as draft for review/launch.',
|
|
74
114
|
inputSchema: {
|
|
75
115
|
draft_id: z.string().uuid(),
|
|
@@ -145,6 +185,15 @@ export function registerDraftTools(server) {
|
|
|
145
185
|
.single();
|
|
146
186
|
if (stepError)
|
|
147
187
|
return { content: [{ type: 'text', text: `Campaign created (${campaign.id}) but failed to create step: ${stepError.message}` }] };
|
|
188
|
+
await recordBrandAssetUsage({
|
|
189
|
+
client: db(),
|
|
190
|
+
projectId,
|
|
191
|
+
source: 'campaign_step',
|
|
192
|
+
resourceId: step.id,
|
|
193
|
+
html: step.body_html,
|
|
194
|
+
}).catch((err) => {
|
|
195
|
+
console.warn('Failed to record brand asset usage for MCP promoted draft:', err);
|
|
196
|
+
});
|
|
148
197
|
const { data: updatedDraft } = await db()
|
|
149
198
|
.from('ai_drafts')
|
|
150
199
|
.update({
|
|
@@ -160,6 +209,13 @@ export function registerDraftTools(server) {
|
|
|
160
209
|
return { content: [{ type: 'text', text: JSON.stringify({ campaign, first_step: step, draft: updatedDraft }, null, 2) }] };
|
|
161
210
|
});
|
|
162
211
|
server.registerTool('approve_draft', {
|
|
212
|
+
title: 'Approve Draft',
|
|
213
|
+
annotations: {
|
|
214
|
+
readOnlyHint: false,
|
|
215
|
+
destructiveHint: false,
|
|
216
|
+
idempotentHint: true,
|
|
217
|
+
openWorldHint: false,
|
|
218
|
+
},
|
|
163
219
|
description: 'Approve a pending AI draft, marking it ready to send',
|
|
164
220
|
inputSchema: {
|
|
165
221
|
draft_id: z.string().uuid(),
|
|
@@ -181,6 +237,13 @@ export function registerDraftTools(server) {
|
|
|
181
237
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
182
238
|
});
|
|
183
239
|
server.registerTool('reject_draft', {
|
|
240
|
+
title: 'Reject Draft',
|
|
241
|
+
annotations: {
|
|
242
|
+
readOnlyHint: false,
|
|
243
|
+
destructiveHint: false,
|
|
244
|
+
idempotentHint: true,
|
|
245
|
+
openWorldHint: false,
|
|
246
|
+
},
|
|
184
247
|
description: 'Reject a pending AI draft with optional feedback',
|
|
185
248
|
inputSchema: {
|
|
186
249
|
draft_id: z.string().uuid(),
|
package/build/tools/forms.js
CHANGED
|
@@ -4,40 +4,73 @@ import { getProjectId } from '../project.js';
|
|
|
4
4
|
const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
5
5
|
export function registerFormTools(server) {
|
|
6
6
|
server.registerTool('list_forms', {
|
|
7
|
+
title: 'List Forms',
|
|
8
|
+
annotations: {
|
|
9
|
+
readOnlyHint: true,
|
|
10
|
+
destructiveHint: false,
|
|
11
|
+
idempotentHint: true,
|
|
12
|
+
openWorldHint: false,
|
|
13
|
+
},
|
|
7
14
|
description: 'List signup forms with field and submission counts.',
|
|
8
15
|
inputSchema: { site_id: z.string().uuid().optional() },
|
|
9
16
|
}, async ({ site_id }) => {
|
|
10
|
-
let query = db().from('
|
|
17
|
+
let query = db().from('signup_forms').select('id, name, slug, fields, status, submissions, site_id, created_at').eq('project_id', getProjectId()).order('created_at', { ascending: false });
|
|
11
18
|
if (site_id)
|
|
12
19
|
query = query.eq('site_id', site_id);
|
|
13
20
|
const { data, error } = await query;
|
|
14
21
|
if (error)
|
|
15
22
|
return j({ error: error.message });
|
|
16
|
-
return j({ forms: (data ?? []).map((form) => ({ ...form, field_count: Array.isArray(form.fields) ? form.fields.length : 0, submission_count: 0 })) });
|
|
23
|
+
return j({ forms: (data ?? []).map((form) => ({ ...form, field_count: Array.isArray(form.fields) ? form.fields.length : 0, submission_count: form.submissions ?? 0 })) });
|
|
17
24
|
});
|
|
18
25
|
server.registerTool('get_form_stats', {
|
|
26
|
+
title: 'Get Form Stats',
|
|
27
|
+
annotations: {
|
|
28
|
+
readOnlyHint: true,
|
|
29
|
+
destructiveHint: false,
|
|
30
|
+
idempotentHint: true,
|
|
31
|
+
openWorldHint: false,
|
|
32
|
+
},
|
|
19
33
|
description: 'Get submission stats for a signup form.',
|
|
20
34
|
inputSchema: { form_id: z.string().uuid() },
|
|
21
35
|
}, async ({ form_id }) => {
|
|
22
|
-
const
|
|
36
|
+
const projectId = getProjectId();
|
|
37
|
+
const { data: form, error: formError } = await db().from('signup_forms').select('id, submissions').eq('id', form_id).eq('project_id', projectId).maybeSingle();
|
|
38
|
+
if (formError)
|
|
39
|
+
return j({ error: formError.message });
|
|
40
|
+
if (!form)
|
|
41
|
+
return j({ error: 'Form not found', code: 'NOT_FOUND' });
|
|
23
42
|
const since = new Date(Date.now() - 7 * 86400000).toISOString();
|
|
24
|
-
const { count: sevenDay } = await db().from('
|
|
25
|
-
|
|
43
|
+
const { count: sevenDay, error: eventsError } = await db().from('contact_events')
|
|
44
|
+
.select('id', { count: 'exact', head: true })
|
|
45
|
+
.eq('project_id', projectId)
|
|
46
|
+
.eq('event_type', 'form_submit')
|
|
47
|
+
.eq('metadata->>form_id', form_id)
|
|
48
|
+
.gte('created_at', since);
|
|
49
|
+
if (eventsError)
|
|
50
|
+
return j({ error: eventsError.message });
|
|
51
|
+
return j({ submissions_total: form.submissions ?? 0, submissions_7d: sevenDay ?? 0, top_sources: [], conversion_rate: null });
|
|
26
52
|
});
|
|
27
53
|
server.registerTool('create_form', {
|
|
54
|
+
title: 'Create Form',
|
|
55
|
+
annotations: {
|
|
56
|
+
readOnlyHint: false,
|
|
57
|
+
destructiveHint: false,
|
|
58
|
+
idempotentHint: false,
|
|
59
|
+
openWorldHint: false,
|
|
60
|
+
},
|
|
28
61
|
description: 'Create a signup form.',
|
|
29
62
|
inputSchema: {
|
|
30
63
|
site_id: z.string().uuid(),
|
|
31
64
|
name: z.string().min(1),
|
|
32
|
-
fields: z.array(z.record(z.unknown())),
|
|
65
|
+
fields: z.array(z.record(z.string(), z.unknown())),
|
|
33
66
|
redirect_url: z.string().url().optional(),
|
|
34
67
|
double_opt_in: z.boolean().optional(),
|
|
35
68
|
},
|
|
36
69
|
}, async ({ site_id, name, fields, redirect_url, double_opt_in }) => {
|
|
37
70
|
const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 80);
|
|
38
|
-
const { data, error } = await db().from('
|
|
39
|
-
.insert({ project_id: getProjectId(), site_id, name, slug, fields, redirect_url: redirect_url ?? null, double_opt_in: double_opt_in ?? false,
|
|
40
|
-
.select('id, name, slug, fields,
|
|
71
|
+
const { data, error } = await db().from('signup_forms')
|
|
72
|
+
.insert({ project_id: getProjectId(), site_id, name, slug, fields, settings: { redirect_url: redirect_url ?? null, double_opt_in: double_opt_in ?? false }, status: 'active' })
|
|
73
|
+
.select('id, name, slug, fields, status')
|
|
41
74
|
.single();
|
|
42
75
|
if (error)
|
|
43
76
|
return j({ error: error.message });
|
package/build/tools/gdpr.js
CHANGED
|
@@ -2,11 +2,18 @@ import { z } from 'zod';
|
|
|
2
2
|
import { db } from '../db.js';
|
|
3
3
|
import { getProjectId } from '../project.js';
|
|
4
4
|
import { logAudit } from '../audit.js';
|
|
5
|
-
import { requestApproval, checkApproval } from '../lib/action-approvals.js';
|
|
5
|
+
import { assertChannelCanExecute, currentChannelContext, requestApproval, checkApproval } from '../lib/action-approvals.js';
|
|
6
6
|
const txt = (text) => ({ content: [{ type: 'text', text }] });
|
|
7
7
|
const j = (data) => txt(JSON.stringify(data, null, 2));
|
|
8
8
|
export function registerGdprTools(server) {
|
|
9
9
|
server.registerTool('delete_subscriber_data', {
|
|
10
|
+
title: 'Delete Subscriber Data',
|
|
11
|
+
annotations: {
|
|
12
|
+
readOnlyHint: false,
|
|
13
|
+
destructiveHint: true,
|
|
14
|
+
idempotentHint: false,
|
|
15
|
+
openWorldHint: false,
|
|
16
|
+
},
|
|
10
17
|
description: 'GDPR Article 17: Permanently delete a subscriber and all their data. Anonymizes email_log, hashes suppressions, cascades to subscriptions/enrollments/tokens. This action is IRREVERSIBLE. Requires human approval via dashboard.',
|
|
11
18
|
inputSchema: {
|
|
12
19
|
contact_id: z.string().uuid().describe('UUID of the subscriber to delete'),
|
|
@@ -23,12 +30,17 @@ export function registerGdprTools(server) {
|
|
|
23
30
|
.maybeSingle();
|
|
24
31
|
if (!contact)
|
|
25
32
|
return txt('Contact not found or does not belong to this project.');
|
|
33
|
+
const approvalParams = { contact_id, email: contact.email, reason };
|
|
26
34
|
// Human approval required (always, even in autonomous mode — protected tier)
|
|
27
35
|
if (!approval_id) {
|
|
28
|
-
const
|
|
36
|
+
const channelContext = currentChannelContext();
|
|
37
|
+
const decision = assertChannelCanExecute('delete_subscriber_data', approvalParams, channelContext);
|
|
38
|
+
if (!decision.allowed)
|
|
39
|
+
return txt(decision.reason ?? 'This channel cannot request this action.');
|
|
40
|
+
const approval = await requestApproval('delete_subscriber_data', approvalParams, undefined, channelContext);
|
|
29
41
|
return j(approval);
|
|
30
42
|
}
|
|
31
|
-
const check = await checkApproval(approval_id, { consume: true });
|
|
43
|
+
const check = await checkApproval(approval_id, { consume: true, action: 'delete_subscriber_data', params: approvalParams });
|
|
32
44
|
if (!check.approved) {
|
|
33
45
|
return txt(`Approval ${approval_id} is ${check.status}. ${check.status === 'pending' ? 'Waiting for human approval via dashboard.' : 'Request a new approval.'}`);
|
|
34
46
|
}
|
|
@@ -44,6 +56,13 @@ export function registerGdprTools(server) {
|
|
|
44
56
|
return j(data);
|
|
45
57
|
});
|
|
46
58
|
server.registerTool('list_deletion_log', {
|
|
59
|
+
title: 'List Deletion Log',
|
|
60
|
+
annotations: {
|
|
61
|
+
readOnlyHint: true,
|
|
62
|
+
destructiveHint: false,
|
|
63
|
+
idempotentHint: true,
|
|
64
|
+
openWorldHint: false,
|
|
65
|
+
},
|
|
47
66
|
description: 'List GDPR deletion audit log entries',
|
|
48
67
|
inputSchema: {
|
|
49
68
|
limit: z.number().int().min(1).max(200).optional().default(50),
|