@sendinel/mcp-server 1.0.10 → 1.0.11

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.
Files changed (57) hide show
  1. package/README.md +35 -22
  2. package/build/anthropic-model.d.ts +30 -0
  3. package/build/anthropic-model.js +26 -1
  4. package/build/db.d.ts +17 -0
  5. package/build/db.js +15 -1
  6. package/build/index.js +6 -0
  7. package/build/lib/action-approvals.js +28 -0
  8. package/build/lib/brand-asset-persist.d.ts +1 -0
  9. package/build/lib/brand-asset-persist.js +20 -1
  10. package/build/lib/composer-blocks.d.ts +25 -25
  11. package/build/lib/composer-blocks.js +25 -5
  12. package/build/lib/connector-table-discovery.d.ts +12 -0
  13. package/build/lib/connector-table-discovery.js +117 -0
  14. package/build/lib/custom-properties-diff.d.ts +16 -0
  15. package/build/lib/custom-properties-diff.js +22 -0
  16. package/build/lib/schedule.d.ts +7 -2
  17. package/build/lib/schedule.js +8 -0
  18. package/build/lib/v1-client.d.ts +3 -0
  19. package/build/lib/v1-client.js +45 -0
  20. package/build/lib/voice.d.ts +7 -30
  21. package/build/lib/voice.js +6 -138
  22. package/build/prompts.d.ts +6 -0
  23. package/build/prompts.js +18 -0
  24. package/build/scoped-token.d.ts +1 -1
  25. package/build/server.d.ts +1 -0
  26. package/build/server.js +65 -6
  27. package/build/tools/analytics.js +97 -0
  28. package/build/tools/brand-kit.js +29 -30
  29. package/build/tools/campaigns.js +326 -52
  30. package/build/tools/cloud-import.d.ts +2 -0
  31. package/build/tools/cloud-import.js +76 -0
  32. package/build/tools/compound.d.ts +11 -0
  33. package/build/tools/compound.js +37 -7
  34. package/build/tools/connector.d.ts +2 -0
  35. package/build/tools/connector.js +280 -0
  36. package/build/tools/contacts.js +744 -16
  37. package/build/tools/content.js +228 -0
  38. package/build/tools/data.js +441 -0
  39. package/build/tools/delivery-ops.js +508 -1
  40. package/build/tools/forms.js +7 -6
  41. package/build/tools/gateway.d.ts +6 -0
  42. package/build/tools/gateway.js +92 -0
  43. package/build/tools/gdpr.js +185 -0
  44. package/build/tools/landing-pages.d.ts +2 -0
  45. package/build/tools/landing-pages.js +202 -0
  46. package/build/tools/lists.d.ts +2 -0
  47. package/build/tools/lists.js +428 -0
  48. package/build/tools/newsletters.js +1 -8
  49. package/build/tools/org.js +549 -33
  50. package/build/tools/segments.js +35 -184
  51. package/build/tools/signatures.d.ts +2 -0
  52. package/build/tools/signatures.js +168 -0
  53. package/build/tools/sites.js +3 -1
  54. package/build/tools/skills.d.ts +2 -0
  55. package/build/tools/skills.js +29 -0
  56. package/build/tools/templates.js +133 -7
  57. package/package.json +13 -12
@@ -77,4 +77,189 @@ export function registerGdprTools(server) {
77
77
  return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
78
78
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
79
79
  });
80
+ server.registerTool('list_ccpa_requests', {
81
+ title: 'List CCPA Requests',
82
+ annotations: {
83
+ readOnlyHint: true,
84
+ destructiveHint: false,
85
+ idempotentHint: true,
86
+ openWorldHint: false,
87
+ },
88
+ description: 'List CCPA (know/delete/opt_out) requests in this project\'s compliance queue. Excludes full data-export and details payloads; use the dashboard to inspect exported request data.',
89
+ inputSchema: {
90
+ status: z.enum(['pending', 'verified', 'completed', 'expired']).optional(),
91
+ request_type: z.enum(['know', 'delete', 'opt_out']).optional(),
92
+ search: z.string().optional().describe('Filter by email (partial match)'),
93
+ limit: z.number().int().min(1).max(200).optional().default(50),
94
+ },
95
+ }, async ({ status, request_type, search, limit }) => {
96
+ let query = db()
97
+ .from('compliance_requests')
98
+ .select('id, email, request_type, status, verified_at, processed_at, completed_at, expires_at, created_at')
99
+ .eq('project_id', getProjectId())
100
+ .eq('regulation', 'ccpa');
101
+ if (status)
102
+ query = query.eq('status', status);
103
+ if (request_type)
104
+ query = query.eq('request_type', request_type);
105
+ if (search)
106
+ query = query.ilike('email', `%${search}%`);
107
+ const { data, error } = await query
108
+ .order('created_at', { ascending: false })
109
+ .limit(limit ?? 50);
110
+ if (error)
111
+ return txt(`Error: ${error.message}`);
112
+ return j({ requests: data ?? [], count: data?.length ?? 0 });
113
+ });
114
+ server.registerTool('action_ccpa_request', {
115
+ title: 'Action CCPA Request',
116
+ annotations: {
117
+ readOnlyHint: false,
118
+ destructiveHint: true,
119
+ idempotentHint: false,
120
+ openWorldHint: false,
121
+ },
122
+ description: 'Move a CCPA request through pending -> verified -> completed. Verification is allowed only while a pending request is unexpired. Completing a delete request permanently erases all subscriber data for its email address and requires protected human approval; call once without approval_id to see exactly what will be deleted, then call again after approval.',
123
+ inputSchema: {
124
+ request_id: z.string().uuid(),
125
+ status: z.enum(['verified', 'completed']),
126
+ approval_id: z.string().uuid().optional().describe('Required to complete a delete request after a human approves the summary in the dashboard.'),
127
+ },
128
+ }, async ({ request_id, status, approval_id }) => {
129
+ const projectId = getProjectId();
130
+ const { data: request, error: fetchErr } = await db()
131
+ .from('compliance_requests')
132
+ .select('id, email, request_type, status, expires_at')
133
+ .eq('id', request_id)
134
+ .eq('project_id', projectId)
135
+ .eq('regulation', 'ccpa')
136
+ .maybeSingle();
137
+ if (fetchErr)
138
+ return txt(`Error: ${fetchErr.message}`);
139
+ if (!request)
140
+ return txt('CCPA request not found or does not belong to this project.');
141
+ if (request.status === 'completed' || request.status === 'expired') {
142
+ return txt(`Request is already "${request.status}" and cannot be actioned further.`);
143
+ }
144
+ if (status === 'verified') {
145
+ if (request.status !== 'pending') {
146
+ return txt(`Request must be "pending" to verify (currently "${request.status}").`);
147
+ }
148
+ const nowIso = new Date().toISOString();
149
+ if (!request.expires_at || new Date(request.expires_at) <= new Date(nowIso)) {
150
+ return txt('Request has expired and can no longer be verified.');
151
+ }
152
+ const { data, error } = await db()
153
+ .from('compliance_requests')
154
+ .update({ status: 'verified', verified_at: nowIso })
155
+ .eq('id', request_id)
156
+ .eq('project_id', projectId)
157
+ .eq('regulation', 'ccpa')
158
+ .eq('status', 'pending')
159
+ .gt('expires_at', nowIso)
160
+ .select('id, email, status, verified_at')
161
+ .maybeSingle();
162
+ if (error)
163
+ return txt(`Error: ${error.message}`);
164
+ if (!data)
165
+ return txt('Request changed or expired before it could be verified. Refresh the queue and try again.');
166
+ void logAudit({
167
+ action: 'ccpa.request_verified',
168
+ resourceType: 'compliance_request',
169
+ resourceId: request_id,
170
+ details: { email: request.email },
171
+ });
172
+ return j({ ok: true, request: data });
173
+ }
174
+ if (request.status !== 'verified') {
175
+ return txt(`Request must be "verified" before it can be completed (currently "${request.status}"). Call this tool with status: "verified" first.`);
176
+ }
177
+ if (request.request_type === 'delete') {
178
+ const approvalParams = {
179
+ request_id,
180
+ status: 'completed',
181
+ request_type: request.request_type,
182
+ email: request.email,
183
+ };
184
+ if (!approval_id) {
185
+ const channelContext = currentChannelContext();
186
+ const decision = assertChannelCanExecute('action_ccpa_request', approvalParams, channelContext);
187
+ if (!decision.allowed)
188
+ return txt(decision.reason ?? 'This channel cannot request this action.');
189
+ return j(await requestApproval('action_ccpa_request', approvalParams, undefined, channelContext));
190
+ }
191
+ const check = await checkApproval(approval_id, {
192
+ consume: true,
193
+ action: 'action_ccpa_request',
194
+ params: approvalParams,
195
+ });
196
+ if (!check.approved) {
197
+ return txt(`Approval ${approval_id} is ${check.status}. ${check.status === 'pending' ? 'Waiting for human approval via dashboard.' : 'Request a new approval.'}`);
198
+ }
199
+ const { data: contact, error: contactErr } = await db()
200
+ .from('subscribers')
201
+ .select('id, email')
202
+ .eq('project_id', projectId)
203
+ .eq('email', request.email)
204
+ .maybeSingle();
205
+ if (contactErr)
206
+ return txt(`Error: ${contactErr.message}`);
207
+ if (contact) {
208
+ const { error: deleteErr } = await db().rpc('delete_contact_gdpr', {
209
+ p_contact_id: contact.id,
210
+ p_reason: 'ccpa_delete_request',
211
+ p_deleted_by: 'mcp_ccpa_action',
212
+ });
213
+ if (deleteErr)
214
+ return txt(`Deletion failed: ${deleteErr.message}`);
215
+ }
216
+ const completedAt = new Date().toISOString();
217
+ const { data, error } = await db()
218
+ .from('compliance_requests')
219
+ .update({
220
+ status: 'completed',
221
+ processed_at: completedAt,
222
+ completed_at: completedAt,
223
+ ...(contact ? {} : { data_export: { message: 'No data found for this email address.' } }),
224
+ })
225
+ .eq('id', request_id)
226
+ .eq('project_id', projectId)
227
+ .eq('regulation', 'ccpa')
228
+ .eq('status', 'verified')
229
+ .select('id, email, status, completed_at')
230
+ .maybeSingle();
231
+ if (error)
232
+ return txt(`Error: ${error.message}`);
233
+ if (!data)
234
+ return txt('Request changed before it could be completed. Refresh the queue before retrying.');
235
+ void logAudit({
236
+ action: 'ccpa.delete_completed',
237
+ resourceType: 'contact',
238
+ resourceId: contact?.id ?? request_id,
239
+ details: { email: request.email, request_id },
240
+ });
241
+ return j({ ok: true, request: data, subscriber_deleted: Boolean(contact) });
242
+ }
243
+ const completedAt = new Date().toISOString();
244
+ const { data, error } = await db()
245
+ .from('compliance_requests')
246
+ .update({ status: 'completed', processed_at: completedAt, completed_at: completedAt })
247
+ .eq('id', request_id)
248
+ .eq('project_id', projectId)
249
+ .eq('regulation', 'ccpa')
250
+ .eq('status', 'verified')
251
+ .select('id, email, status, completed_at')
252
+ .maybeSingle();
253
+ if (error)
254
+ return txt(`Error: ${error.message}`);
255
+ if (!data)
256
+ return txt('Request changed before it could be completed. Refresh the queue before retrying.');
257
+ void logAudit({
258
+ action: 'ccpa.request_completed',
259
+ resourceType: 'compliance_request',
260
+ resourceId: request_id,
261
+ details: { email: request.email, request_type: request.request_type },
262
+ });
263
+ return j({ ok: true, request: data });
264
+ });
80
265
  }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerLandingPageTools(server: McpServer): void;
@@ -0,0 +1,202 @@
1
+ import { z } from 'zod';
2
+ import { db } from '../db.js';
3
+ import { getProjectId } from '../project.js';
4
+ import { logAudit } from '../audit.js';
5
+ const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
6
+ const SLUG_RE = /^[a-z0-9][a-z0-9\-]{0,62}$/;
7
+ const VALID_STATUSES = ['draft', 'published', 'archived'];
8
+ const LIST_COLUMNS = 'id, title, slug, site_id, status, form_id, blocks, meta_title, meta_description, conversion_event, views, conversions, background_color, accent_color, custom_css, created_at, updated_at, published_at';
9
+ function slugify(text) {
10
+ return text
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9]+/g, '-')
13
+ .replace(/^-+|-+$/g, '')
14
+ .slice(0, 63);
15
+ }
16
+ export function registerLandingPageTools(server) {
17
+ server.registerTool('list_landing_pages', {
18
+ title: 'List Landing Pages',
19
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
20
+ description: 'List project-scoped landing pages, optionally filtered by site.',
21
+ inputSchema: { site_id: z.string().uuid().optional() },
22
+ }, async ({ site_id }) => {
23
+ let query = db()
24
+ .from('landing_pages')
25
+ .select(LIST_COLUMNS)
26
+ .eq('project_id', getProjectId());
27
+ if (site_id)
28
+ query = query.eq('site_id', site_id);
29
+ const { data, error } = await query.order('created_at', { ascending: false });
30
+ if (error)
31
+ return j({ error: error.message });
32
+ return j({ pages: data ?? [] });
33
+ });
34
+ server.registerTool('get_landing_page', {
35
+ title: 'Get Landing Page',
36
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
37
+ description: 'Get one project-scoped landing page, including its full block content.',
38
+ inputSchema: { page_id: z.string().uuid() },
39
+ }, async ({ page_id }) => {
40
+ const { data, error } = await db()
41
+ .from('landing_pages')
42
+ .select('*')
43
+ .eq('project_id', getProjectId())
44
+ .eq('id', page_id)
45
+ .maybeSingle();
46
+ if (error)
47
+ return j({ error: error.message });
48
+ if (!data)
49
+ return j({ error: 'Landing page not found', code: 'NOT_FOUND' });
50
+ return j(data);
51
+ });
52
+ server.registerTool('create_landing_page', {
53
+ title: 'Create Landing Page',
54
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
55
+ description: 'Create a draft landing page. Publish it with update_landing_page by setting status to published.',
56
+ inputSchema: {
57
+ title: z.string().trim().min(1).max(200),
58
+ slug: z.string().trim().regex(SLUG_RE, 'Lowercase alphanumeric + hyphens, max 63 chars').optional(),
59
+ site_id: z.string().uuid().optional(),
60
+ form_id: z.string().uuid().optional(),
61
+ blocks: z.array(z.record(z.string(), z.unknown())).optional().describe('Ordered page content blocks'),
62
+ meta_title: z.string().optional(),
63
+ meta_description: z.string().optional(),
64
+ conversion_event: z.string().optional(),
65
+ background_color: z.string().optional(),
66
+ accent_color: z.string().optional(),
67
+ custom_css: z.string().optional(),
68
+ },
69
+ }, async ({ title, slug, site_id, form_id, blocks, meta_title, meta_description, conversion_event, background_color, accent_color, custom_css }) => {
70
+ const projectId = getProjectId();
71
+ const normalizedTitle = title.trim();
72
+ const normalizedSlug = (slug ?? slugify(normalizedTitle)).trim();
73
+ if (!SLUG_RE.test(normalizedSlug))
74
+ return j({ error: 'Invalid slug (lowercase alphanumeric + hyphens, max 63 chars)' });
75
+ const insert = {
76
+ project_id: projectId,
77
+ title: normalizedTitle,
78
+ slug: normalizedSlug,
79
+ };
80
+ if (site_id)
81
+ insert.site_id = site_id;
82
+ if (form_id)
83
+ insert.form_id = form_id;
84
+ if (blocks)
85
+ insert.blocks = blocks;
86
+ if (meta_title)
87
+ insert.meta_title = meta_title;
88
+ if (meta_description)
89
+ insert.meta_description = meta_description;
90
+ if (conversion_event)
91
+ insert.conversion_event = conversion_event;
92
+ if (background_color)
93
+ insert.background_color = background_color;
94
+ if (accent_color)
95
+ insert.accent_color = accent_color;
96
+ if (custom_css)
97
+ insert.custom_css = custom_css;
98
+ const { data, error } = await db()
99
+ .from('landing_pages')
100
+ .insert(insert)
101
+ .select()
102
+ .single();
103
+ if (error) {
104
+ if (error.code === '23505')
105
+ return j({ error: 'A page with this slug already exists' });
106
+ return j({ error: error.message });
107
+ }
108
+ await logAudit({
109
+ action: 'landing_page.created',
110
+ resourceType: 'landing_page',
111
+ resourceId: data.id,
112
+ details: { title: data.title, slug: data.slug, status: data.status },
113
+ });
114
+ return j(data);
115
+ });
116
+ server.registerTool('update_landing_page', {
117
+ title: 'Update Landing Page',
118
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
119
+ description: 'Update a landing page. Setting status to published stamps published_at; pass site_id or form_id as null to clear it.',
120
+ inputSchema: {
121
+ page_id: z.string().uuid(),
122
+ title: z.string().trim().min(1).max(200).optional(),
123
+ slug: z.string().trim().regex(SLUG_RE, 'Lowercase alphanumeric + hyphens, max 63 chars').optional(),
124
+ status: z.enum(VALID_STATUSES).optional(),
125
+ site_id: z.string().uuid().nullable().optional(),
126
+ form_id: z.string().uuid().nullable().optional(),
127
+ blocks: z.array(z.record(z.string(), z.unknown())).optional(),
128
+ meta_title: z.string().optional(),
129
+ meta_description: z.string().optional(),
130
+ conversion_event: z.string().optional(),
131
+ background_color: z.string().optional(),
132
+ accent_color: z.string().optional(),
133
+ custom_css: z.string().optional(),
134
+ },
135
+ }, async ({ page_id, title, slug, status, site_id, form_id, blocks, meta_title, meta_description, conversion_event, background_color, accent_color, custom_css }) => {
136
+ const projectId = getProjectId();
137
+ const updates = { updated_at: new Date().toISOString() };
138
+ if (title !== undefined)
139
+ updates.title = title.trim();
140
+ if (slug !== undefined)
141
+ updates.slug = slug.trim();
142
+ if (status !== undefined) {
143
+ updates.status = status;
144
+ if (status === 'published')
145
+ updates.published_at = new Date().toISOString();
146
+ }
147
+ if (site_id !== undefined)
148
+ updates.site_id = site_id;
149
+ if (form_id !== undefined)
150
+ updates.form_id = form_id;
151
+ if (blocks !== undefined)
152
+ updates.blocks = blocks;
153
+ if (meta_title !== undefined)
154
+ updates.meta_title = meta_title;
155
+ if (meta_description !== undefined)
156
+ updates.meta_description = meta_description;
157
+ if (conversion_event !== undefined)
158
+ updates.conversion_event = conversion_event;
159
+ if (background_color !== undefined)
160
+ updates.background_color = background_color;
161
+ if (accent_color !== undefined)
162
+ updates.accent_color = accent_color;
163
+ if (custom_css !== undefined)
164
+ updates.custom_css = custom_css;
165
+ const { data, error } = await db()
166
+ .from('landing_pages')
167
+ .update(updates)
168
+ .eq('id', page_id)
169
+ .eq('project_id', projectId)
170
+ .select()
171
+ .single();
172
+ if (error) {
173
+ if (error.code === '23505')
174
+ return j({ error: 'A page with this slug already exists' });
175
+ return j({ error: error.message });
176
+ }
177
+ await logAudit({
178
+ action: 'landing_page.updated',
179
+ resourceType: 'landing_page',
180
+ resourceId: data.id,
181
+ details: { title: data.title, slug: data.slug, status: data.status },
182
+ });
183
+ return j(data);
184
+ });
185
+ server.registerTool('delete_landing_page', {
186
+ title: 'Delete Landing Page',
187
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
188
+ description: 'Permanently delete a landing page. Set status to archived instead to unpublish without deleting.',
189
+ inputSchema: { page_id: z.string().uuid() },
190
+ }, async ({ page_id }) => {
191
+ const projectId = getProjectId();
192
+ const { error } = await db()
193
+ .from('landing_pages')
194
+ .delete()
195
+ .eq('id', page_id)
196
+ .eq('project_id', projectId);
197
+ if (error)
198
+ return j({ error: error.message });
199
+ await logAudit({ action: 'landing_page.deleted', resourceType: 'landing_page', resourceId: page_id });
200
+ return j({ deleted: true });
201
+ });
202
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerListTools(server: McpServer): void;