@sendinel/mcp-server 1.0.63 → 1.0.65
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/server.d.ts +9 -0
- package/build/server.js +27 -3
- 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/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
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
import { withApproval } from '../lib/action-approvals.js';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { db } from '../db.js';
|
|
4
5
|
import { getProjectId } from '../project.js';
|
|
@@ -558,8 +559,8 @@ export function registerSocialPostTools(server) {
|
|
|
558
559
|
openWorldHint: true,
|
|
559
560
|
},
|
|
560
561
|
description: 'Archive a social post so it disappears from active planner and list views.',
|
|
561
|
-
inputSchema: { post_id: z.string().uuid() },
|
|
562
|
-
}, async ({ post_id }) => {
|
|
562
|
+
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.'), post_id: z.string().uuid() },
|
|
563
|
+
}, withApproval('delete_social_post', async ({ post_id }) => {
|
|
563
564
|
const { data, error } = await db()
|
|
564
565
|
.from('social_posts')
|
|
565
566
|
.update({ status: 'archived', updated_at: new Date().toISOString() })
|
|
@@ -571,5 +572,5 @@ export function registerSocialPostTools(server) {
|
|
|
571
572
|
return txt(`Error: ${error.message}`);
|
|
572
573
|
fireWebhookEvent('social.post.archived', { social_post_id: data.id, title: data.title }, getProjectId()).catch(() => { });
|
|
573
574
|
return j(data);
|
|
574
|
-
});
|
|
575
|
+
}));
|
|
575
576
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
import { withApproval } from '../lib/action-approvals.js';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { parseLinkedInCommentUrl, parseLinkedInPostUrl, parseLinkedInTargetUrl } from '@sendinel/domain';
|
|
4
5
|
import { db, requestContext } from '../db.js';
|
|
@@ -144,10 +145,10 @@ export function registerSocialReplyTools(server) {
|
|
|
144
145
|
inputSchema: { post_url_or_urn: z.string().min(1), text: replyText, target_member_id: z.string().uuid().optional() },
|
|
145
146
|
}, async ({ post_url_or_urn, text, target_member_id }) => proposePersonal('post_comment', post_url_or_urn, text, target_member_id));
|
|
146
147
|
server.registerTool('reply_to_comment', {
|
|
147
|
-
title: 'Propose LinkedIn Comment Reply', description: 'Create a review card for a personal LinkedIn reply. Never posts.',
|
|
148
|
+
title: 'Propose LinkedIn Comment Reply', description: 'Create a review card for a personal LinkedIn reply. Never posts. Requires human approval: the first call returns an approve_url and does nothing; call again with the same arguments plus approval_id once approved.',
|
|
148
149
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
149
|
-
inputSchema: { comment_url_or_urn: z.string().min(1), text: replyText, target_member_id: z.string().uuid().optional() },
|
|
150
|
-
}, async ({ comment_url_or_urn, text, target_member_id }) => proposePersonal('comment_reply', comment_url_or_urn, text, target_member_id));
|
|
150
|
+
inputSchema: { comment_url_or_urn: z.string().min(1), text: replyText, target_member_id: z.string().uuid().optional(), 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.') },
|
|
151
|
+
}, withApproval('reply_to_comment', async ({ comment_url_or_urn, text, target_member_id }) => proposePersonal('comment_reply', comment_url_or_urn, text, target_member_id)));
|
|
151
152
|
server.registerTool('request_social_reply', {
|
|
152
153
|
title: 'Request LinkedIn Reply Proposal',
|
|
153
154
|
description: 'Create a review card for a personal LinkedIn reply from an external trigger or email notification. Never posts; a human Accept posts it.',
|
package/build/tools/templates.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withApproval } from '../lib/action-approvals.js';
|
|
1
2
|
import { snapshotTemplateVersion, versionHasBodySnapshot } from '@sendinel/domain';
|
|
2
3
|
import { z } from 'zod';
|
|
3
4
|
import { logAudit } from '../audit.js';
|
|
@@ -377,8 +378,8 @@ export function registerTemplateTools(server) {
|
|
|
377
378
|
openWorldHint: false,
|
|
378
379
|
},
|
|
379
380
|
description: 'Archive an email template. System templates cannot be deleted.',
|
|
380
|
-
inputSchema: { id: z.string().uuid() },
|
|
381
|
-
}, async ({ id }) => {
|
|
381
|
+
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() },
|
|
382
|
+
}, withApproval('delete_template', async ({ id }) => {
|
|
382
383
|
const projectId = getProjectId();
|
|
383
384
|
const { data: existing, error: existingErr } = await db().from('campaign_templates')
|
|
384
385
|
.select('id, project_id')
|
|
@@ -395,7 +396,7 @@ export function registerTemplateTools(server) {
|
|
|
395
396
|
if (error)
|
|
396
397
|
return j({ error: error.message });
|
|
397
398
|
return j({ ok: true, deleted: id });
|
|
398
|
-
});
|
|
399
|
+
}));
|
|
399
400
|
server.registerTool('restore_template_version', {
|
|
400
401
|
title: 'Restore Template Version',
|
|
401
402
|
annotations: {
|
package/build/tools/webhooks.js
CHANGED
|
@@ -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';
|
|
@@ -34,11 +35,12 @@ export function registerWebhookTools(server) {
|
|
|
34
35
|
},
|
|
35
36
|
description: `Register a webhook URL to receive push notifications when events occur. Sendinel signs each payload with HMAC-SHA256 so you can verify authenticity. Valid events: ${VALID_EVENTS.join(', ')}`,
|
|
36
37
|
inputSchema: {
|
|
38
|
+
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.'),
|
|
37
39
|
url: z.string().url().describe('HTTPS endpoint that will receive POST requests'),
|
|
38
40
|
events: z.array(z.enum(VALID_EVENTS)).min(1).describe('Events to subscribe to'),
|
|
39
41
|
description: z.string().optional().describe('Human-readable label, e.g. "Production orchestrator"'),
|
|
40
42
|
},
|
|
41
|
-
}, async ({ url, events, description }) => {
|
|
43
|
+
}, withApproval('subscribe_webhook', async ({ url, events, description }) => {
|
|
42
44
|
// SSRF protection
|
|
43
45
|
const validation = validateWebhookUrl(url);
|
|
44
46
|
if (!validation.valid)
|
|
@@ -63,7 +65,7 @@ export function registerWebhookTools(server) {
|
|
|
63
65
|
message: `Webhook registered. Store the secret — it won't be shown again. Sendinel will POST to ${url} with X-Sendinel-Signature header for verification.`,
|
|
64
66
|
verification_example: `const expected = crypto.createHmac('sha256', secret).update(requestBody).digest('hex'); if (header === 'sha256=' + expected) { /* valid */ }`,
|
|
65
67
|
});
|
|
66
|
-
});
|
|
68
|
+
}));
|
|
67
69
|
server.registerTool('list_webhook_subscriptions', {
|
|
68
70
|
title: 'List Webhook Subscriptions',
|
|
69
71
|
annotations: {
|
|
@@ -99,13 +101,14 @@ export function registerWebhookTools(server) {
|
|
|
99
101
|
},
|
|
100
102
|
description: 'Update a webhook subscription — change URL, events, or active status.',
|
|
101
103
|
inputSchema: {
|
|
104
|
+
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.'),
|
|
102
105
|
subscription_id: z.string().uuid(),
|
|
103
106
|
url: z.string().url().optional(),
|
|
104
107
|
events: z.array(z.enum(VALID_EVENTS)).min(1).optional(),
|
|
105
108
|
active: z.boolean().optional(),
|
|
106
109
|
description: z.string().optional(),
|
|
107
110
|
},
|
|
108
|
-
}, async ({ subscription_id, url, events, active, description }) => {
|
|
111
|
+
}, withApproval('update_webhook_subscription', async ({ subscription_id, url, events, active, description }) => {
|
|
109
112
|
if (url) {
|
|
110
113
|
const validation = validateWebhookUrl(url);
|
|
111
114
|
if (!validation.valid)
|
|
@@ -130,7 +133,7 @@ export function registerWebhookTools(server) {
|
|
|
130
133
|
if (error)
|
|
131
134
|
return txt(`Error: ${error.message}`);
|
|
132
135
|
return j(data);
|
|
133
|
-
});
|
|
136
|
+
}));
|
|
134
137
|
server.registerTool('delete_webhook_subscription', {
|
|
135
138
|
title: 'Delete Webhook Subscription',
|
|
136
139
|
annotations: {
|
|
@@ -141,9 +144,10 @@ export function registerWebhookTools(server) {
|
|
|
141
144
|
},
|
|
142
145
|
description: 'Permanently delete a webhook subscription. This also deletes all delivery history for this subscription.',
|
|
143
146
|
inputSchema: {
|
|
147
|
+
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.'),
|
|
144
148
|
subscription_id: z.string().uuid(),
|
|
145
149
|
},
|
|
146
|
-
}, async ({ subscription_id }) => {
|
|
150
|
+
}, withApproval('delete_webhook_subscription', async ({ subscription_id }) => {
|
|
147
151
|
const { error } = await db()
|
|
148
152
|
.from('webhook_subscriptions')
|
|
149
153
|
.delete()
|
|
@@ -152,5 +156,5 @@ export function registerWebhookTools(server) {
|
|
|
152
156
|
if (error)
|
|
153
157
|
return txt(`Error: ${error.message}`);
|
|
154
158
|
return txt(`Subscription ${subscription_id} deleted.`);
|
|
155
|
-
});
|
|
159
|
+
}));
|
|
156
160
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.65",
|
|
4
4
|
"description": "AI-controlled email operations control plane for campaigns, contacts, segments, analytics, and multi-provider sending via MCP.",
|
|
5
5
|
"mcpName": "ai.sendinel/mcp-server",
|
|
6
6
|
"type": "module",
|