@sendinel/mcp-server 1.0.63 → 1.0.64
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/lib/action-approvals.d.ts +22 -0
- package/build/lib/action-approvals.js +94 -1
- package/build/lib/approval-exempt.d.ts +13 -0
- package/build/lib/approval-exempt.js +134 -0
- package/build/tools/ab-testing.js +4 -2
- package/build/tools/analytics.js +4 -2
- package/build/tools/automations.js +6 -3
- package/build/tools/brand-kit.js +4 -2
- package/build/tools/campaigns.js +23 -18
- package/build/tools/contacts.js +17 -13
- package/build/tools/content.js +7 -5
- package/build/tools/data.js +13 -10
- package/build/tools/delivery-ops.js +4 -3
- package/build/tools/landing-pages.js +7 -5
- package/build/tools/lists.js +4 -4
- package/build/tools/newsletters.js +8 -5
- package/build/tools/org.js +13 -10
- package/build/tools/segments.js +4 -2
- package/build/tools/signatures.js +4 -3
- package/build/tools/sms.js +11 -7
- package/build/tools/social-posts.js +4 -3
- package/build/tools/social-replies.js +4 -3
- package/build/tools/templates.js +4 -3
- package/build/tools/webhooks.js +10 -6
- package/package.json +1 -1
package/build/tools/contacts.js
CHANGED
|
@@ -7,7 +7,7 @@ import { db, projectClientForWriteback } from '../db.js';
|
|
|
7
7
|
import { getProjectId } from '../project.js';
|
|
8
8
|
import { checkPlanLimit } from '../plan-limits.js';
|
|
9
9
|
import { logConsent, logAudit } from '../audit.js';
|
|
10
|
-
import { assertChannelCanExecute, currentChannelContext, requestApproval, checkApproval } from '../lib/action-approvals.js';
|
|
10
|
+
import { assertChannelCanExecute, currentChannelContext, requestApproval, checkApproval, withApproval } from '../lib/action-approvals.js';
|
|
11
11
|
import { decrypt } from '../crypto.js';
|
|
12
12
|
import { normalizeActiveCampaignAccountUrl } from '../lib/url-validation.js';
|
|
13
13
|
import { diffCustomProperties } from '../lib/custom-properties-diff.js';
|
|
@@ -1059,9 +1059,10 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1059
1059
|
},
|
|
1060
1060
|
description: 'Delete a saved subscriber view owned by the current project.',
|
|
1061
1061
|
inputSchema: {
|
|
1062
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
1062
1063
|
view_id: z.string().uuid(),
|
|
1063
1064
|
},
|
|
1064
|
-
}, async ({ view_id }) => {
|
|
1065
|
+
}, withApproval('delete_subscriber_view', async ({ view_id }) => {
|
|
1065
1066
|
const { data, error } = await db()
|
|
1066
1067
|
.from('subscriber_list_views')
|
|
1067
1068
|
.delete()
|
|
@@ -1074,7 +1075,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1074
1075
|
if (!data)
|
|
1075
1076
|
return txt('View not found');
|
|
1076
1077
|
return j({ deleted: true, view_id });
|
|
1077
|
-
});
|
|
1078
|
+
}));
|
|
1078
1079
|
server.registerTool('list_companies', {
|
|
1079
1080
|
title: 'List Companies',
|
|
1080
1081
|
annotations: {
|
|
@@ -1231,6 +1232,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1231
1232
|
},
|
|
1232
1233
|
description: 'Bulk import contacts and subscribe them to a site. Max 200 per call.',
|
|
1233
1234
|
inputSchema: {
|
|
1235
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
1234
1236
|
contacts: z.array(z.object({
|
|
1235
1237
|
email: z.string().email(),
|
|
1236
1238
|
first_name: z.string().optional(),
|
|
@@ -1238,7 +1240,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1238
1240
|
})).min(1).max(200),
|
|
1239
1241
|
site_id: z.string().uuid(),
|
|
1240
1242
|
},
|
|
1241
|
-
}, async ({ contacts, site_id }) => {
|
|
1243
|
+
}, withApproval('import_subscribers', async ({ contacts, site_id }) => {
|
|
1242
1244
|
const projectId = getProjectId();
|
|
1243
1245
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1244
1246
|
// Distribution list / system address block — same patterns as dashboard email-validation.ts
|
|
@@ -1314,7 +1316,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1314
1316
|
const created = contactRows.filter((c) => !existingEmails.has(c.email)).length;
|
|
1315
1317
|
const updated = contactRows.length - created;
|
|
1316
1318
|
return { content: [{ type: 'text', text: JSON.stringify({ created, updated, total: contactRows.length }, null, 2) }] };
|
|
1317
|
-
});
|
|
1319
|
+
}));
|
|
1318
1320
|
server.registerTool('unsubscribe_subscriber', {
|
|
1319
1321
|
title: 'Unsubscribe Subscriber',
|
|
1320
1322
|
annotations: {
|
|
@@ -1862,10 +1864,11 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1862
1864
|
},
|
|
1863
1865
|
description: 'Merge custom_properties into many contacts at once. Same merge semantics as update_subscriber, applied across a batch.',
|
|
1864
1866
|
inputSchema: {
|
|
1867
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
1865
1868
|
contact_ids: z.array(z.string().uuid()).min(1),
|
|
1866
1869
|
custom_properties: z.record(z.string(), z.unknown()).describe('Merged into each contact\'s existing custom_properties; a key set to null deletes that key'),
|
|
1867
1870
|
},
|
|
1868
|
-
}, async ({ contact_ids, custom_properties }) => {
|
|
1871
|
+
}, withApproval('bulk_update_subscribers', async ({ contact_ids, custom_properties }) => {
|
|
1869
1872
|
const projectId = getProjectId();
|
|
1870
1873
|
const orgId = await getProjectOrgId(projectId);
|
|
1871
1874
|
const fields = await loadContactPropertyDefinitions(projectId);
|
|
@@ -1921,7 +1924,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1921
1924
|
await writeBackMcpContact(projectId, contact, { custom_properties: merged });
|
|
1922
1925
|
}
|
|
1923
1926
|
return j({ updated, total: contact_ids.length });
|
|
1924
|
-
});
|
|
1927
|
+
}));
|
|
1925
1928
|
server.registerTool('estimate_segment_size', {
|
|
1926
1929
|
title: 'Estimate Segment Size',
|
|
1927
1930
|
annotations: {
|
|
@@ -2141,11 +2144,12 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2141
2144
|
idempotentHint: false,
|
|
2142
2145
|
openWorldHint: false,
|
|
2143
2146
|
},
|
|
2144
|
-
description: 'Remove an email address from the suppression list',
|
|
2147
|
+
description: 'Remove an email address from the suppression list. Requires human approval: the first call returns an approve_url and does nothing; call again with the same arguments plus approval_id once approved.',
|
|
2145
2148
|
inputSchema: {
|
|
2149
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
2146
2150
|
email: z.string().email(),
|
|
2147
2151
|
},
|
|
2148
|
-
}, async ({ email }) => {
|
|
2152
|
+
}, withApproval('remove_suppression', async ({ email }) => {
|
|
2149
2153
|
const { data, error } = await db()
|
|
2150
2154
|
.from('suppressions')
|
|
2151
2155
|
.delete()
|
|
@@ -2158,7 +2162,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2158
2162
|
if (!data)
|
|
2159
2163
|
return txt(`No suppression found for ${email}.`);
|
|
2160
2164
|
return j({ removed: true, email, previous_reason: data.reason });
|
|
2161
|
-
});
|
|
2165
|
+
}));
|
|
2162
2166
|
server.registerTool('find_duplicates', {
|
|
2163
2167
|
title: 'Find Duplicates',
|
|
2164
2168
|
annotations: {
|
|
@@ -2496,8 +2500,8 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2496
2500
|
title: 'Delete Custom Field',
|
|
2497
2501
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
2498
2502
|
description: 'Delete a custom field definition without deleting existing contact values stored under that key.',
|
|
2499
|
-
inputSchema: { id: z.string().uuid() },
|
|
2500
|
-
}, async ({ id }) => {
|
|
2503
|
+
inputSchema: { approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'), id: z.string().uuid() },
|
|
2504
|
+
}, withApproval('delete_custom_field', async ({ id }) => {
|
|
2501
2505
|
const projectId = getProjectId();
|
|
2502
2506
|
const { data, error } = await db()
|
|
2503
2507
|
.from('custom_field_definitions')
|
|
@@ -2517,7 +2521,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2517
2521
|
details: { key: data.key },
|
|
2518
2522
|
});
|
|
2519
2523
|
return j({ deleted: true, id: data.id, key: data.key });
|
|
2520
|
-
});
|
|
2524
|
+
}));
|
|
2521
2525
|
server.registerTool('get_contact_neighborhood', {
|
|
2522
2526
|
title: 'Get Contact Neighborhood',
|
|
2523
2527
|
annotations: {
|
package/build/tools/content.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withApproval } from '../lib/action-approvals.js';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
import { adminDb, db } from '../db.js';
|
|
3
4
|
import { getProjectId } from '../project.js';
|
|
@@ -234,13 +235,13 @@ export function registerContentTools(server) {
|
|
|
234
235
|
openWorldHint: false,
|
|
235
236
|
},
|
|
236
237
|
description: 'Delete a reusable content block.',
|
|
237
|
-
inputSchema: { block_id: z.string().uuid() },
|
|
238
|
-
}, async ({ block_id }) => {
|
|
238
|
+
inputSchema: { approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'), block_id: z.string().uuid() },
|
|
239
|
+
}, withApproval('delete_content_block', async ({ block_id }) => {
|
|
239
240
|
const { error } = await db().from('content_blocks').delete().eq('project_id', getProjectId()).eq('id', block_id);
|
|
240
241
|
if (error)
|
|
241
242
|
return j({ error: error.message });
|
|
242
243
|
return j({ deleted: true });
|
|
243
|
-
});
|
|
244
|
+
}));
|
|
244
245
|
server.registerTool('list_content_sources', {
|
|
245
246
|
title: 'List Content Sources',
|
|
246
247
|
annotations: {
|
|
@@ -294,6 +295,7 @@ export function registerContentTools(server) {
|
|
|
294
295
|
},
|
|
295
296
|
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.',
|
|
296
297
|
inputSchema: {
|
|
298
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
297
299
|
title: z.string().min(1).max(300),
|
|
298
300
|
body: z.string().optional(),
|
|
299
301
|
excerpt: z.string().max(1000).optional(),
|
|
@@ -308,7 +310,7 @@ export function registerContentTools(server) {
|
|
|
308
310
|
auto_publish: z.boolean().optional(),
|
|
309
311
|
publish_to: z.string().max(200).optional(),
|
|
310
312
|
},
|
|
311
|
-
}, async (args) => {
|
|
313
|
+
}, withApproval('create_blog_post', async (args) => {
|
|
312
314
|
const { data: proj, error: projErr } = await adminDb().from('projects').select('org_id').eq('id', getProjectId()).single();
|
|
313
315
|
if (projErr || !proj?.org_id)
|
|
314
316
|
return j({ error: 'Organization not found for project' });
|
|
@@ -363,7 +365,7 @@ export function registerContentTools(server) {
|
|
|
363
365
|
if (error)
|
|
364
366
|
return j({ error: error.message, code: error.code });
|
|
365
367
|
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.' } : {}) });
|
|
366
|
-
});
|
|
368
|
+
}));
|
|
367
369
|
server.registerTool('list_blog_posts', {
|
|
368
370
|
title: 'List Blog Posts',
|
|
369
371
|
annotations: {
|
package/build/tools/data.js
CHANGED
|
@@ -4,7 +4,7 @@ import { z } from 'zod';
|
|
|
4
4
|
import { adminDb, db } from '../db.js';
|
|
5
5
|
import { decrypt, encrypt } from '../crypto.js';
|
|
6
6
|
import { normalizeActiveCampaignAccountUrl } from '../lib/url-validation.js';
|
|
7
|
-
import { assertChannelCanExecute, checkApproval, currentChannelContext, requestApproval } from '../lib/action-approvals.js';
|
|
7
|
+
import { assertChannelCanExecute, checkApproval, currentChannelContext, requestApproval, withApproval } from '../lib/action-approvals.js';
|
|
8
8
|
import { getProjectId } from '../project.js';
|
|
9
9
|
import { logAudit } from '../audit.js';
|
|
10
10
|
const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
@@ -562,11 +562,12 @@ export function registerDataTools(server) {
|
|
|
562
562
|
},
|
|
563
563
|
description: 'Disconnect an external platform connection for the current project organization and remove stored credentials.',
|
|
564
564
|
inputSchema: {
|
|
565
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
565
566
|
org_id: z.string().uuid().optional(),
|
|
566
567
|
platform: z.enum(CONNECTED_PLATFORMS).optional(),
|
|
567
568
|
connection_id: z.string().uuid().optional(),
|
|
568
569
|
},
|
|
569
|
-
}, async ({ org_id, platform, connection_id }) => {
|
|
570
|
+
}, withApproval('disconnect_platform', async ({ org_id, platform, connection_id }) => {
|
|
570
571
|
try {
|
|
571
572
|
if (!platform && !connection_id)
|
|
572
573
|
return j({ error: 'platform or connection_id is required' });
|
|
@@ -596,7 +597,7 @@ export function registerDataTools(server) {
|
|
|
596
597
|
catch (error) {
|
|
597
598
|
return j({ error: error instanceof Error ? error.message : 'Failed to disconnect platform' });
|
|
598
599
|
}
|
|
599
|
-
});
|
|
600
|
+
}));
|
|
600
601
|
server.registerTool('list_platform_events', {
|
|
601
602
|
title: 'List Platform Events',
|
|
602
603
|
annotations: {
|
|
@@ -978,11 +979,12 @@ export function registerDataTools(server) {
|
|
|
978
979
|
},
|
|
979
980
|
description: 'Create an API key and return the secret once.',
|
|
980
981
|
inputSchema: {
|
|
982
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
981
983
|
name: z.string().min(1),
|
|
982
984
|
slug: z.string().optional(),
|
|
983
985
|
scopes: z.array(z.string()).optional().default(['read']),
|
|
984
986
|
},
|
|
985
|
-
}, async ({ name, slug, scopes = ['read'] }) => {
|
|
987
|
+
}, withApproval('create_api_key', async ({ name, slug, scopes = ['read'] }) => {
|
|
986
988
|
// MCP API-key sessions can be unbound to a user, so they cannot prove an
|
|
987
989
|
// acting org role. Admin keys must be created through the dashboard.
|
|
988
990
|
if (scopes.includes('admin')) {
|
|
@@ -997,7 +999,7 @@ export function registerDataTools(server) {
|
|
|
997
999
|
if (error)
|
|
998
1000
|
return j({ error: error.message });
|
|
999
1001
|
return j({ key_id: data.id, name: data.name, slug: data.slug, secret });
|
|
1000
|
-
});
|
|
1002
|
+
}));
|
|
1001
1003
|
server.registerTool('revoke_api_key', {
|
|
1002
1004
|
title: 'Revoke API Key',
|
|
1003
1005
|
annotations: {
|
|
@@ -1007,13 +1009,13 @@ export function registerDataTools(server) {
|
|
|
1007
1009
|
openWorldHint: false,
|
|
1008
1010
|
},
|
|
1009
1011
|
description: 'Revoke an API key.',
|
|
1010
|
-
inputSchema: { key_id: z.string().uuid() },
|
|
1011
|
-
}, async ({ key_id }) => {
|
|
1012
|
+
inputSchema: { approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'), key_id: z.string().uuid() },
|
|
1013
|
+
}, withApproval('revoke_api_key', async ({ key_id }) => {
|
|
1012
1014
|
const { error } = await db().from('api_keys').update({ revoked_at: new Date().toISOString() }).eq('project_id', getProjectId()).eq('id', key_id);
|
|
1013
1015
|
if (error)
|
|
1014
1016
|
return j({ error: error.message });
|
|
1015
1017
|
return j({ revoked: true });
|
|
1016
|
-
});
|
|
1018
|
+
}));
|
|
1017
1019
|
server.registerTool('migrate_campaigns_from', {
|
|
1018
1020
|
title: 'Migrate Campaigns From',
|
|
1019
1021
|
annotations: {
|
|
@@ -1195,10 +1197,11 @@ export function registerDataTools(server) {
|
|
|
1195
1197
|
},
|
|
1196
1198
|
description: 'Export all tenant data classes for portability. Returns inline data for small exports or a signed archive URL for large exports.',
|
|
1197
1199
|
inputSchema: {
|
|
1200
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
1198
1201
|
siteId: z.string().uuid(),
|
|
1199
1202
|
format: z.enum(['json', 'csv']).optional().default('json'),
|
|
1200
1203
|
},
|
|
1201
|
-
}, async ({ siteId, format = 'json' }) => {
|
|
1204
|
+
}, withApproval('tenant_export_full', async ({ siteId, format = 'json' }) => {
|
|
1202
1205
|
const projectId = getProjectId();
|
|
1203
1206
|
const since = new Date(Date.now() - 90 * 24 * 3600 * 1000).toISOString();
|
|
1204
1207
|
const client = db();
|
|
@@ -1284,5 +1287,5 @@ export function registerDataTools(server) {
|
|
|
1284
1287
|
expires_at: new Date(Date.now() + 24 * 3600 * 1000).toISOString(),
|
|
1285
1288
|
record_counts: recordCounts,
|
|
1286
1289
|
});
|
|
1287
|
-
});
|
|
1290
|
+
}));
|
|
1288
1291
|
}
|
|
@@ -3,7 +3,7 @@ import dns from 'node:dns/promises';
|
|
|
3
3
|
import { cancelSendQueueRows, findReplayableEnrollmentDlqRows, listSendQueueRows, replayEnrollmentDlqRows, retrySendQueueRows, } from '@sendinel/domain';
|
|
4
4
|
import { logAudit } from '../audit.js';
|
|
5
5
|
import { adminDb, db } from '../db.js';
|
|
6
|
-
import { assertChannelCanExecute, checkApproval, currentChannelContext, requestApproval } from '../lib/action-approvals.js';
|
|
6
|
+
import { assertChannelCanExecute, checkApproval, currentChannelContext, requestApproval, withApproval } from '../lib/action-approvals.js';
|
|
7
7
|
import { callSendinelV1 } from '../lib/v1-client.js';
|
|
8
8
|
import { getProjectId } from '../project.js';
|
|
9
9
|
const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
@@ -507,13 +507,14 @@ export function registerDeliveryOpsTools(server) {
|
|
|
507
507
|
},
|
|
508
508
|
description: 'Re-enter dead-lettered campaign enrollments into active sending, which can cause real emails to send on the next queue tick. Call once without confirmed to preview the matched count, then call again with confirmed=true to replay them.',
|
|
509
509
|
inputSchema: {
|
|
510
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
510
511
|
site_id: z.string().uuid(),
|
|
511
512
|
max_count: z.number().int().min(1).max(1000).default(100).optional(),
|
|
512
513
|
failed_after: z.string().datetime().optional(),
|
|
513
514
|
confirmed: z.boolean().optional().default(false)
|
|
514
515
|
.describe('Two-call confirmation. First call previews the matched rows; confirmed=true replays them.'),
|
|
515
516
|
},
|
|
516
|
-
}, async ({ site_id, max_count = 100, failed_after, confirmed = false }) => {
|
|
517
|
+
}, withApproval('replay_enrollment_dlq_rows', async ({ site_id, max_count = 100, failed_after, confirmed = false }) => {
|
|
517
518
|
const projectId = getProjectId();
|
|
518
519
|
if (!confirmed) {
|
|
519
520
|
try {
|
|
@@ -561,7 +562,7 @@ export function registerDeliveryOpsTools(server) {
|
|
|
561
562
|
catch (error) {
|
|
562
563
|
return j({ error: error instanceof Error ? error.message : 'Replay failed' });
|
|
563
564
|
}
|
|
564
|
-
});
|
|
565
|
+
}));
|
|
565
566
|
server.registerTool('get_scoring_rules', {
|
|
566
567
|
title: 'Get Scoring Rules',
|
|
567
568
|
annotations: {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withApproval } from '../lib/action-approvals.js';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
import { db } from '../db.js';
|
|
3
4
|
import { getProjectId } from '../project.js';
|
|
@@ -118,6 +119,7 @@ export function registerLandingPageTools(server) {
|
|
|
118
119
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
119
120
|
description: 'Update a landing page. Setting status to published stamps published_at; pass site_id or form_id as null to clear it.',
|
|
120
121
|
inputSchema: {
|
|
122
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
121
123
|
page_id: z.string().uuid(),
|
|
122
124
|
title: z.string().trim().min(1).max(200).optional(),
|
|
123
125
|
slug: z.string().trim().regex(SLUG_RE, 'Lowercase alphanumeric + hyphens, max 63 chars').optional(),
|
|
@@ -132,7 +134,7 @@ export function registerLandingPageTools(server) {
|
|
|
132
134
|
accent_color: z.string().optional(),
|
|
133
135
|
custom_css: z.string().optional(),
|
|
134
136
|
},
|
|
135
|
-
}, async ({ page_id, title, slug, status, site_id, form_id, blocks, meta_title, meta_description, conversion_event, background_color, accent_color, custom_css }) => {
|
|
137
|
+
}, withApproval('update_landing_page', async ({ page_id, title, slug, status, site_id, form_id, blocks, meta_title, meta_description, conversion_event, background_color, accent_color, custom_css }) => {
|
|
136
138
|
const projectId = getProjectId();
|
|
137
139
|
const updates = { updated_at: new Date().toISOString() };
|
|
138
140
|
if (title !== undefined)
|
|
@@ -181,13 +183,13 @@ export function registerLandingPageTools(server) {
|
|
|
181
183
|
details: { title: data.title, slug: data.slug, status: data.status },
|
|
182
184
|
});
|
|
183
185
|
return j(data);
|
|
184
|
-
});
|
|
186
|
+
}));
|
|
185
187
|
server.registerTool('delete_landing_page', {
|
|
186
188
|
title: 'Delete Landing Page',
|
|
187
189
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
188
190
|
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
|
+
inputSchema: { approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'), page_id: z.string().uuid() },
|
|
192
|
+
}, withApproval('delete_landing_page', async ({ page_id }) => {
|
|
191
193
|
const projectId = getProjectId();
|
|
192
194
|
const { error } = await db()
|
|
193
195
|
.from('landing_pages')
|
|
@@ -198,5 +200,5 @@ export function registerLandingPageTools(server) {
|
|
|
198
200
|
return j({ error: error.message });
|
|
199
201
|
await logAudit({ action: 'landing_page.deleted', resourceType: 'landing_page', resourceId: page_id });
|
|
200
202
|
return j({ deleted: true });
|
|
201
|
-
});
|
|
203
|
+
}));
|
|
202
204
|
}
|
package/build/tools/lists.js
CHANGED
|
@@ -3,7 +3,7 @@ import { applySegmentRules, buildSegmentBaseQuery, evaluateSegmentRules, normali
|
|
|
3
3
|
import { db } from '../db.js';
|
|
4
4
|
import { getProjectId } from '../project.js';
|
|
5
5
|
import { logAudit } from '../audit.js';
|
|
6
|
-
import { assertChannelCanExecute, checkApproval, currentChannelContext, requestApproval, } from '../lib/action-approvals.js';
|
|
6
|
+
import { assertChannelCanExecute, checkApproval, currentChannelContext, requestApproval, withApproval, } from '../lib/action-approvals.js';
|
|
7
7
|
const LIST_MEMBER_BATCH_SIZE = 500;
|
|
8
8
|
const txt = (text) => ({ content: [{ type: 'text', text }] });
|
|
9
9
|
const j = (data) => txt(JSON.stringify(data, null, 2));
|
|
@@ -181,8 +181,8 @@ export function registerListTools(server) {
|
|
|
181
181
|
title: 'Delete Contact List',
|
|
182
182
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
183
183
|
description: 'Permanently delete a static contact list and its memberships.',
|
|
184
|
-
inputSchema: { list_id: z.string().uuid() },
|
|
185
|
-
}, async ({ list_id }) => {
|
|
184
|
+
inputSchema: { approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'), list_id: z.string().uuid() },
|
|
185
|
+
}, withApproval('delete_list', async ({ list_id }) => {
|
|
186
186
|
const projectId = getProjectId();
|
|
187
187
|
const { error } = await db()
|
|
188
188
|
.from('contact_lists')
|
|
@@ -193,7 +193,7 @@ export function registerListTools(server) {
|
|
|
193
193
|
return txt(`Error: ${error.message}`);
|
|
194
194
|
await logAudit({ action: 'list.deleted', resourceType: 'list', resourceId: list_id });
|
|
195
195
|
return j({ deleted: true, list_id });
|
|
196
|
-
});
|
|
196
|
+
}));
|
|
197
197
|
server.registerTool('add_list_members', {
|
|
198
198
|
title: 'Add List Members',
|
|
199
199
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withApproval } from '../lib/action-approvals.js';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
import { db } from '../db.js';
|
|
3
4
|
import { getProjectId } from '../project.js';
|
|
@@ -354,11 +355,12 @@ export function registerNewsletterTools(server) {
|
|
|
354
355
|
},
|
|
355
356
|
description: 'Schedule a newsletter issue through the campaign send pipeline.',
|
|
356
357
|
inputSchema: {
|
|
358
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
357
359
|
newsletter_id: z.string().uuid(),
|
|
358
360
|
issue_id: z.string().uuid(),
|
|
359
361
|
scheduled_at: z.string(),
|
|
360
362
|
},
|
|
361
|
-
}, async ({ newsletter_id, issue_id, scheduled_at }) => {
|
|
363
|
+
}, withApproval('schedule_newsletter_issue', async ({ newsletter_id, issue_id, scheduled_at }) => {
|
|
362
364
|
const parsed = parseFutureDate(scheduled_at);
|
|
363
365
|
if ('error' in parsed)
|
|
364
366
|
return j({ error: parsed.error, code: 'INVALID_FIELD' });
|
|
@@ -381,7 +383,7 @@ export function registerNewsletterTools(server) {
|
|
|
381
383
|
if (error)
|
|
382
384
|
return j({ error: error.message });
|
|
383
385
|
return j({ issue: data, campaign_id: campaign.campaignId, enrolled: campaign.enrolled });
|
|
384
|
-
});
|
|
386
|
+
}));
|
|
385
387
|
server.registerTool('send_newsletter_issue', {
|
|
386
388
|
title: 'Send Newsletter Issue',
|
|
387
389
|
annotations: {
|
|
@@ -390,12 +392,13 @@ export function registerNewsletterTools(server) {
|
|
|
390
392
|
idempotentHint: false,
|
|
391
393
|
openWorldHint: true,
|
|
392
394
|
},
|
|
393
|
-
description: 'Send a newsletter issue now through the campaign send pipeline and create archive metadata.',
|
|
395
|
+
description: 'Send a newsletter issue now through the campaign send pipeline and create archive metadata. Requires human approval: the first call returns an approve_url and does nothing; call again with the same arguments plus approval_id once approved.',
|
|
394
396
|
inputSchema: {
|
|
397
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
395
398
|
newsletter_id: z.string().uuid(),
|
|
396
399
|
issue_id: z.string().uuid(),
|
|
397
400
|
},
|
|
398
|
-
}, async ({ newsletter_id, issue_id }) => {
|
|
401
|
+
}, withApproval('send_newsletter_issue', async ({ newsletter_id, issue_id }) => {
|
|
399
402
|
const projectId = getProjectId();
|
|
400
403
|
const loaded = await loadSendableIssue(newsletter_id, issue_id, projectId);
|
|
401
404
|
if ('error' in loaded)
|
|
@@ -436,7 +439,7 @@ export function registerNewsletterTools(server) {
|
|
|
436
439
|
if (error)
|
|
437
440
|
return j({ error: error.message });
|
|
438
441
|
return j({ issue: data, campaign_id: campaign.campaignId, enrolled: campaign.enrolled, blog_post_id: post?.id ?? null, archive_url: post?.external_url ?? null });
|
|
439
|
-
});
|
|
442
|
+
}));
|
|
440
443
|
server.registerTool('subscribe_to_newsletter', {
|
|
441
444
|
title: 'Subscribe To Newsletter',
|
|
442
445
|
annotations: {
|
package/build/tools/org.js
CHANGED
|
@@ -3,7 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { logAudit } from '../audit.js';
|
|
4
4
|
import { adminDb, db } from '../db.js';
|
|
5
5
|
import { getProjectId } from '../project.js';
|
|
6
|
-
import { assertChannelCanExecute, checkApproval, currentChannelContext, requestApproval } from '../lib/action-approvals.js';
|
|
6
|
+
import { assertChannelCanExecute, checkApproval, currentChannelContext, requestApproval, withApproval } from '../lib/action-approvals.js';
|
|
7
7
|
import { callSendinelV1 } from '../lib/v1-client.js';
|
|
8
8
|
import { readAllRows } from '@sendinel/domain';
|
|
9
9
|
const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
@@ -175,8 +175,8 @@ export function registerOrgTools(server) {
|
|
|
175
175
|
openWorldHint: false,
|
|
176
176
|
},
|
|
177
177
|
description: 'Create a team invitation for the current project organization.',
|
|
178
|
-
inputSchema: { email: z.string().email(), role: z.enum(['owner', 'admin', 'member']).default('member') },
|
|
179
|
-
}, async ({ email, role }) => {
|
|
178
|
+
inputSchema: { approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'), email: z.string().email(), role: z.enum(['owner', 'admin', 'member']).default('member') },
|
|
179
|
+
}, withApproval('invite_team_member', async ({ email, role }) => {
|
|
180
180
|
const { data: project } = await db().from('projects').select('org_id').eq('id', getProjectId()).maybeSingle();
|
|
181
181
|
if (!project?.org_id)
|
|
182
182
|
return j({ error: 'Project has no organization', code: 'NO_ORG' });
|
|
@@ -189,7 +189,7 @@ export function registerOrgTools(server) {
|
|
|
189
189
|
if (error)
|
|
190
190
|
return j({ error: error.message });
|
|
191
191
|
return j({ invitation_id: data.id, expires_at: data.expires_at });
|
|
192
|
-
});
|
|
192
|
+
}));
|
|
193
193
|
server.registerTool('remove_team_member', {
|
|
194
194
|
title: 'Remove Team Member',
|
|
195
195
|
annotations: {
|
|
@@ -348,11 +348,12 @@ export function registerOrgTools(server) {
|
|
|
348
348
|
},
|
|
349
349
|
description: "Replace a team member's per-project access within the org (full replace, not a merge). Org owners have implicit access to all projects and cannot be scoped this way.",
|
|
350
350
|
inputSchema: {
|
|
351
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
351
352
|
user_id: z.string().uuid(),
|
|
352
353
|
projects: z.array(memberProjectAssignmentSchema),
|
|
353
354
|
org_id: z.string().uuid().optional(),
|
|
354
355
|
},
|
|
355
|
-
}, async ({ user_id, projects, org_id }) => {
|
|
356
|
+
}, withApproval('set_member_project_access', async ({ user_id, projects, org_id }) => {
|
|
356
357
|
const context = await resolveCurrentOrgId(org_id);
|
|
357
358
|
if (!context.ok)
|
|
358
359
|
return j({ error: context.error, code: context.code });
|
|
@@ -409,7 +410,7 @@ export function registerOrgTools(server) {
|
|
|
409
410
|
details: { org_id: context.orgId, project_ids: requestedIds },
|
|
410
411
|
}).catch(() => { });
|
|
411
412
|
return j({ ok: true, user_id, project_ids: requestedIds });
|
|
412
|
-
});
|
|
413
|
+
}));
|
|
413
414
|
server.registerTool('list_invitations', {
|
|
414
415
|
title: 'List Invitations',
|
|
415
416
|
annotations: {
|
|
@@ -495,10 +496,11 @@ export function registerOrgTools(server) {
|
|
|
495
496
|
},
|
|
496
497
|
description: 'Revoke a pending org invitation before it is accepted.',
|
|
497
498
|
inputSchema: {
|
|
499
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
498
500
|
invitation_id: z.string().uuid(),
|
|
499
501
|
org_id: z.string().uuid().optional(),
|
|
500
502
|
},
|
|
501
|
-
}, async ({ invitation_id, org_id }) => {
|
|
503
|
+
}, withApproval('revoke_invitation', async ({ invitation_id, org_id }) => {
|
|
502
504
|
const context = await resolveCurrentOrgId(org_id);
|
|
503
505
|
if (!context.ok)
|
|
504
506
|
return j({ error: context.error, code: context.code });
|
|
@@ -526,7 +528,7 @@ export function registerOrgTools(server) {
|
|
|
526
528
|
details: { org_id: context.orgId, email: invitation.email, role: invitation.role },
|
|
527
529
|
}).catch(() => { });
|
|
528
530
|
return j({ ok: true });
|
|
529
|
-
});
|
|
531
|
+
}));
|
|
530
532
|
server.registerTool('get_plan_usage', {
|
|
531
533
|
title: 'Get Plan Usage',
|
|
532
534
|
annotations: {
|
|
@@ -910,16 +912,17 @@ export function registerOrgTools(server) {
|
|
|
910
912
|
},
|
|
911
913
|
description: 'Start moving a sending domain between supported providers using the existing provider-switch flow.',
|
|
912
914
|
inputSchema: {
|
|
915
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
913
916
|
site_id: z.string().uuid(),
|
|
914
917
|
to_provider: z.enum(PROVIDER_SWITCH_V1_PROVIDERS),
|
|
915
918
|
},
|
|
916
|
-
}, async ({ site_id, to_provider }) => {
|
|
919
|
+
}, withApproval('start_provider_switch', async ({ site_id, to_provider }) => {
|
|
917
920
|
const result = await callProviderSwitchApi('/api/dashboard/provider-switch/start', {
|
|
918
921
|
method: 'POST',
|
|
919
922
|
body: { site_id, to_provider },
|
|
920
923
|
});
|
|
921
924
|
return result.ok ? j(result.data) : j({ error: result.error });
|
|
922
|
-
});
|
|
925
|
+
}));
|
|
923
926
|
server.registerTool('get_provider_switch_status', {
|
|
924
927
|
title: 'Get Provider Switch Status',
|
|
925
928
|
annotations: {
|
package/build/tools/segments.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withApproval } from '../lib/action-approvals.js';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
import Anthropic from '@anthropic-ai/sdk';
|
|
3
4
|
import { db } from '../db.js';
|
|
@@ -152,9 +153,10 @@ export function registerSegmentTools(server) {
|
|
|
152
153
|
},
|
|
153
154
|
description: 'Delete a saved segment. Campaigns using this segment will lose targeting.',
|
|
154
155
|
inputSchema: {
|
|
156
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
155
157
|
segment_id: z.string().uuid(),
|
|
156
158
|
},
|
|
157
|
-
}, async ({ segment_id }) => {
|
|
159
|
+
}, withApproval('delete_segment', async ({ segment_id }) => {
|
|
158
160
|
const { error } = await db()
|
|
159
161
|
.from('segments')
|
|
160
162
|
.delete()
|
|
@@ -163,7 +165,7 @@ export function registerSegmentTools(server) {
|
|
|
163
165
|
if (error)
|
|
164
166
|
return txt(`Error: ${error.message}`);
|
|
165
167
|
return j({ deleted: true, segment_id });
|
|
166
|
-
});
|
|
168
|
+
}));
|
|
167
169
|
server.registerTool('preview_segment', {
|
|
168
170
|
title: 'Preview Segment',
|
|
169
171
|
annotations: {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withApproval } from '../lib/action-approvals.js';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
import { db } from '../db.js';
|
|
3
4
|
import { getProjectId } from '../project.js';
|
|
@@ -137,8 +138,8 @@ export function registerSignatureTools(server) {
|
|
|
137
138
|
title: 'Delete Signature',
|
|
138
139
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
|
|
139
140
|
description: 'Permanently delete a project-scoped sending signature block.',
|
|
140
|
-
inputSchema: { id: z.string().uuid() },
|
|
141
|
-
}, async ({ id }) => {
|
|
141
|
+
inputSchema: { approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'), id: z.string().uuid() },
|
|
142
|
+
}, withApproval('delete_signature', async ({ id }) => {
|
|
142
143
|
const projectId = getProjectId();
|
|
143
144
|
const { data: existing, error: existingError } = await db()
|
|
144
145
|
.from('signature_blocks')
|
|
@@ -164,5 +165,5 @@ export function registerSignatureTools(server) {
|
|
|
164
165
|
details: { name: existing.name, site_id: existing.site_id, via: 'mcp' },
|
|
165
166
|
});
|
|
166
167
|
return j({ ok: true, deleted: id });
|
|
167
|
-
});
|
|
168
|
+
}));
|
|
168
169
|
}
|
package/build/tools/sms.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
|
+
import { withApproval } from '../lib/action-approvals.js';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { decrypt } from '../crypto.js';
|
|
4
5
|
import { db } from '../db.js';
|
|
@@ -308,13 +309,14 @@ export function registerSmsTools(server) {
|
|
|
308
309
|
},
|
|
309
310
|
description: 'Manually record an opt-in or opt-out for a contact (e.g., from a paper form or phone conversation).',
|
|
310
311
|
inputSchema: {
|
|
312
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
311
313
|
contact_id: z.string().uuid(),
|
|
312
314
|
status: z.enum(['opt_in', 'opt_out']),
|
|
313
315
|
source: z.string().optional(),
|
|
314
316
|
consent_text: z.string().optional(),
|
|
315
317
|
reason: z.string().optional(),
|
|
316
318
|
},
|
|
317
|
-
}, async ({ contact_id, status, source, consent_text, reason }) => {
|
|
319
|
+
}, withApproval('set_contact_sms_consent', async ({ contact_id, status, source, consent_text, reason }) => {
|
|
318
320
|
const projectId = getProjectId();
|
|
319
321
|
const orgId = await getProjectOrgId(projectId);
|
|
320
322
|
if (!orgId)
|
|
@@ -366,7 +368,7 @@ export function registerSmsTools(server) {
|
|
|
366
368
|
return j({ error: suppressionError.message });
|
|
367
369
|
}
|
|
368
370
|
return j({ ok: true, contact_id, phone_e164: contact.phone_e164, status });
|
|
369
|
-
});
|
|
371
|
+
}));
|
|
370
372
|
server.registerTool('preview_sms_cost', {
|
|
371
373
|
title: 'Preview SMS Cost',
|
|
372
374
|
annotations: {
|
|
@@ -403,6 +405,7 @@ export function registerSmsTools(server) {
|
|
|
403
405
|
},
|
|
404
406
|
description: "Launch an SMS campaign or broadcast to a segment. Same shape as email campaigns but with channel='sms'.",
|
|
405
407
|
inputSchema: {
|
|
408
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
406
409
|
site_id: z.string().uuid(),
|
|
407
410
|
name: z.string().min(1).max(200),
|
|
408
411
|
body: z.string().min(1),
|
|
@@ -411,7 +414,7 @@ export function registerSmsTools(server) {
|
|
|
411
414
|
scheduled_at: z.string().optional(),
|
|
412
415
|
activate: z.boolean().optional().default(false),
|
|
413
416
|
},
|
|
414
|
-
}, async ({ site_id, name, body, type, segment_id, scheduled_at, activate }) => {
|
|
417
|
+
}, withApproval('send_sms_campaign', async ({ site_id, name, body, type, segment_id, scheduled_at, activate }) => {
|
|
415
418
|
const projectId = getProjectId();
|
|
416
419
|
const { data: campaign, error: campaignError } = await db()
|
|
417
420
|
.from('campaigns')
|
|
@@ -452,7 +455,7 @@ export function registerSmsTools(server) {
|
|
|
452
455
|
segment_id: segment_id ?? null,
|
|
453
456
|
note: segment_id ? 'Campaign created. Enroll the target segment through the campaign workflow to start delivery.' : 'Campaign created as SMS-ready.',
|
|
454
457
|
});
|
|
455
|
-
});
|
|
458
|
+
}));
|
|
456
459
|
server.registerTool('send_sms', {
|
|
457
460
|
title: 'Send SMS',
|
|
458
461
|
annotations: {
|
|
@@ -461,15 +464,16 @@ export function registerSmsTools(server) {
|
|
|
461
464
|
idempotentHint: false,
|
|
462
465
|
openWorldHint: true,
|
|
463
466
|
},
|
|
464
|
-
description: 'Send a single SMS through the active SMS provider to a contact or raw phone number. Validates consent and quiet hours before sending.',
|
|
467
|
+
description: 'Send a single SMS through the active SMS provider to a contact or raw phone number. Validates consent and quiet hours before sending. Requires human approval: the first call returns an approve_url and does nothing; call again with the same arguments plus approval_id once approved.',
|
|
465
468
|
inputSchema: {
|
|
469
|
+
approval_id: z.string().uuid().optional().describe('Get this by calling once without it, then have a human approve the returned approve_url. Re-call with the same arguments plus this approval_id to run the action.'),
|
|
466
470
|
body: z.string().min(1),
|
|
467
471
|
to_number: z.string().optional(),
|
|
468
472
|
contact_id: z.string().uuid().optional(),
|
|
469
473
|
site_id: z.string().uuid().optional(),
|
|
470
474
|
classification: z.enum(['marketing', 'transactional', 'conversational']).optional().default('marketing'),
|
|
471
475
|
},
|
|
472
|
-
}, async ({ body, to_number, contact_id, site_id, classification }) => {
|
|
476
|
+
}, withApproval('send_sms', async ({ body, to_number, contact_id, site_id, classification }) => {
|
|
473
477
|
const projectId = getProjectId();
|
|
474
478
|
const orgId = await getProjectOrgId(projectId);
|
|
475
479
|
if (!orgId)
|
|
@@ -534,5 +538,5 @@ export function registerSmsTools(server) {
|
|
|
534
538
|
encoding: sendResult.encoding,
|
|
535
539
|
estimated_cost_cents: sendResult.estimated_cost_cents,
|
|
536
540
|
});
|
|
537
|
-
});
|
|
541
|
+
}));
|
|
538
542
|
}
|