@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.
@@ -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.',
@@ -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: {
@@ -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.63",
3
+ "version": "1.0.64",
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",