@sendinel/mcp-server 1.0.52 → 1.0.54

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/README.md CHANGED
@@ -12,7 +12,7 @@ AI-controlled email operations control plane for campaigns, contacts, segments,
12
12
 
13
13
  ## Tool groups
14
14
 
15
- Sendinel currently exposes 34 tool groups and 321 tools (gateway mode adds 3 meta-tools). These counts are checked against the tool registry in CI, so this table matches what ships.
15
+ Sendinel currently exposes 34 tool groups and 322 tools (gateway mode adds 3 meta-tools). These counts are checked against the tool registry in CI, so this table matches what ships.
16
16
 
17
17
  | Group | Count | Purpose |
18
18
  |-------|-------|---------|
@@ -38,7 +38,7 @@ Sendinel currently exposes 34 tool groups and 321 tools (gateway mode adds 3 met
38
38
  | `landing-pages` | 5 | Landing page CRUD |
39
39
  | `lists` | 7 | Static contact lists and membership management |
40
40
  | `newsletters` | 12 | Newsletter publications, issues, subscribers, scheduling, sending, stats |
41
- | `org` | 22 | Team members, plan usage/limits, audit log, model registry, white-label, support tickets, provider switching |
41
+ | `org` | 23 | Team members, plan usage/limits, audit log, model registry, white-label, support tickets, provider switching |
42
42
  | `segments` | 6 | Audience builder: create, edit, natural-language generation, preview |
43
43
  | `short-links` | 3 | Short links, click stats, QR codes |
44
44
  | `signatures` | 4 | Sending signature blocks |
package/build/server.js CHANGED
@@ -160,6 +160,7 @@ const ADMIN_TOOLS = new Set([
160
160
  'delete_signature',
161
161
  'merge_subscribers', 'delete_webhook_subscription',
162
162
  'set_white_label_settings', 'add_white_label_domain',
163
+ 'post_ticket_reply',
163
164
  ]);
164
165
  const SCOPE_HIERARCHY = { read: 1, write: 2, admin: 3 };
165
166
  export function getRequiredScope(name) {
@@ -716,8 +716,9 @@ export function registerCampaignTools(server) {
716
716
  survey_id: z.string().uuid().optional(),
717
717
  brand_asset_ids: z.array(z.string().uuid()).optional().describe('Stable brand_media_assets ids explicitly referenced by this step.'),
718
718
  skip_if_event: z.string().trim().min(1).optional().describe('Skip this step at send time if the contact already has an event with this name; the journey continues.'),
719
+ send_only_if_event: z.string().trim().min(1).optional().describe('Send this step only if the contact has an event with this name by send time; otherwise skip it and continue the journey.'),
719
720
  },
720
- }, async ({ campaign_id, step_order, subject, body_html, body_text, blocks, delay_days, delay_hours, variant_label, survey_id, brand_asset_ids, skip_if_event }) => {
721
+ }, async ({ campaign_id, step_order, subject, body_html, body_text, blocks, delay_days, delay_hours, variant_label, survey_id, brand_asset_ids, skip_if_event, send_only_if_event }) => {
721
722
  const renderVars = {};
722
723
  if (survey_id) {
723
724
  const surveyLink = await resolveSurveyShortLink(db(), getProjectId(), survey_id);
@@ -732,7 +733,7 @@ export function registerCampaignTools(server) {
732
733
  const renderedText = body_text !== undefined && survey_id ? renderTemplateVars(body_text, renderVars) : body_text;
733
734
  const { data, error } = await db()
734
735
  .from('campaign_steps')
735
- .insert({ campaign_id, step_order, subject: renderedSubject, body_html: renderedHtml, body_text: renderedText ?? (normalizedBlocks?.length ? blocksToText(normalizedBlocks) : undefined), blocks: normalizedBlocks ?? null, delay_days, delay_hours, variant_label: variant_label ?? 'A', skip_if_event: skip_if_event ?? null })
736
+ .insert({ campaign_id, step_order, subject: renderedSubject, body_html: renderedHtml, body_text: renderedText ?? (normalizedBlocks?.length ? blocksToText(normalizedBlocks) : undefined), blocks: normalizedBlocks ?? null, delay_days, delay_hours, variant_label: variant_label ?? 'A', skip_if_event: skip_if_event ?? null, send_only_if_event: send_only_if_event ?? null })
736
737
  .select()
737
738
  .single();
738
739
  if (error)
@@ -3096,7 +3097,7 @@ Return ONLY valid JSON with this exact shape:
3096
3097
  return j({ error: campaignErr?.message ?? 'Campaign not found', code: 'NOT_FOUND' });
3097
3098
  const { data, error } = await db()
3098
3099
  .from('campaign_steps')
3099
- .select('id, step_order, subject, body_html, body_text, blocks, delay_days, delay_hours, skip_if_event, created_at, updated_at')
3100
+ .select('id, step_order, subject, body_html, body_text, blocks, delay_days, delay_hours, skip_if_event, send_only_if_event, created_at, updated_at')
3100
3101
  .eq('campaign_id', campaign_id)
3101
3102
  .order('step_order', { ascending: true });
3102
3103
  if (error)
@@ -3123,6 +3124,7 @@ Return ONLY valid JSON with this exact shape:
3123
3124
  step_order: z.number().int().min(0).optional(),
3124
3125
  survey_id: z.string().uuid().optional(),
3125
3126
  skip_if_event: z.string().trim().min(1).nullable().optional().describe('Event name that skips this step at send time; null clears the rule.'),
3127
+ send_only_if_event: z.string().trim().min(1).nullable().optional().describe('Event required for this step to send; null clears the rule.'),
3126
3128
  },
3127
3129
  }, async ({ step_id, ...updates }) => {
3128
3130
  const projectId = getProjectId();
@@ -3163,7 +3165,7 @@ Return ONLY valid JSON with this exact shape:
3163
3165
  .from('campaign_steps')
3164
3166
  .update({ ...renderedUpdates, updated_at: new Date().toISOString() })
3165
3167
  .eq('id', step_id)
3166
- .select('id, step_order, subject, delay_days, delay_hours, skip_if_event, updated_at')
3168
+ .select('id, step_order, subject, delay_days, delay_hours, skip_if_event, send_only_if_event, updated_at')
3167
3169
  .single();
3168
3170
  if (error)
3169
3171
  return j({ error: error.message });
@@ -882,6 +882,24 @@ export function registerOrgTools(server) {
882
882
  });
883
883
  return result.ok ? j(result.data) : j({ error: result.error });
884
884
  });
885
+ server.registerTool('post_ticket_reply', {
886
+ title: 'Post Approved Ticket Reply',
887
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
888
+ description: 'Post the exact Kevin-approved reply to a support ticket. Platform-admin only; requires an approval reference and SHA-256 of the exact body. Does not email or resolve the ticket.',
889
+ inputSchema: {
890
+ ticket_ref: z.string().regex(/^SND-\d+$/i),
891
+ body: z.string().min(1),
892
+ approval_ref: z.string().min(1),
893
+ body_sha256: z.string(),
894
+ reopen: z.boolean().optional(),
895
+ },
896
+ }, async ({ ticket_ref, body, approval_ref, body_sha256, reopen }) => {
897
+ const result = await callSendinelV1(`/api/v1/support/tickets/${encodeURIComponent(ticket_ref)}/reply`, {
898
+ method: 'POST',
899
+ body: { body, approval_ref, body_sha256, reopen },
900
+ });
901
+ return result.ok ? j(result.data) : j({ error: result.error });
902
+ });
885
903
  server.registerTool('start_provider_switch', {
886
904
  title: 'Start Provider Switch',
887
905
  annotations: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.52",
3
+ "version": "1.0.54",
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",