@sendinel/mcp-server 1.0.209 → 1.0.211

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
@@ -18,7 +18,7 @@ preserves REST outcomes, and redacts failures. See the
18
18
  [event-triggered campaign guide](../../docs/event-triggered-campaigns.md) for
19
19
  inputs, idempotency, subscription requirements, and conditional step examples.
20
20
 
21
- Sendinel currently exposes 35 tool groups and 349 tools (gateway mode exposes 4 meta-tools, sharing how_to with full mode). These counts are checked against the tool registry in CI, so this table matches what ships.
21
+ Sendinel currently exposes 35 tool groups and 352 tools (gateway mode exposes 4 meta-tools, sharing how_to with full mode). These counts are checked against the tool registry in CI, so this table matches what ships.
22
22
 
23
23
  | Group | Count | Purpose |
24
24
  |-------|-------|---------|
@@ -48,7 +48,7 @@ Sendinel currently exposes 35 tool groups and 349 tools (gateway mode exposes 4
48
48
  | `org` | 24 | Team members, plan usage/limits, audit log, model registry, white-label, support tickets, outbound digest, provider switching |
49
49
  | `segments` | 6 | Audience builder: create, edit, natural-language generation, preview |
50
50
  | `short-links` | 3 | Short links, click stats, QR codes |
51
- | `signatures` | 4 | Sending signature blocks |
51
+ | `signatures` | 7 | Sending signature blocks |
52
52
  | `sites` | 7 | Site, sender, and brand voice management |
53
53
  | `skills` | 3 | Operator skills catalog |
54
54
  | `sms` | 7 | SMS send, log, consent, inbound |
@@ -365,7 +365,11 @@ export async function requestApproval(action, params, apiKeyId, channelContext,
365
365
  const detailSummary = buildApprovalSummary(action, frozenParams, display);
366
366
  const preview = buildApprovalPreview({ action, params: frozenParams, summary: detailSummary,
367
367
  ...(display && 'recipient_count' in display ? { recipientCount: display.recipient_count } : {}) });
368
- const summary = `${preview.plain_summary.split('\n')[0]}\n${detailSummary}`;
368
+ const consequence = preview.plain_summary.split('\n')[0];
369
+ // Named snapshot summaries already include the consequence from the preview builder.
370
+ const summary = detailSummary.split('\n')[0] === consequence
371
+ ? detailSummary
372
+ : `${consequence}\n${detailSummary}`;
369
373
  const { data, error } = await approvalStore()
370
374
  .from('action_approvals')
371
375
  .insert({
@@ -8,6 +8,20 @@ const projectRule = 'On hosted /mcp, pass project_id on each call. It is require
8
8
  const previewRule = 'preview_email uses the real send renderer with sample contact data. send_test_email with dry_run:true uses the real renderer without sending or needing approval, including in standard mode. Stdio requires SENDINEL_API_KEY.';
9
9
  const sender = { n: 1, tool: 'get_sites', args_example: {}, note: 'Choose the intended sender from the returned array; do not assume the first sender is correct. Replace example content and addresses with the user’s own choices.' };
10
10
  export const HOW_TO_RECIPES = [
11
+ {
12
+ id: 'brand-kit-signature', title: 'Preview and save a brand-kit email signature',
13
+ when_to_use: 'Set up or edit a signature footer for future AI-generated drafts using stored brand details.',
14
+ synonyms: ['signature', 'footer', 'email signature', 'brand kit signature'],
15
+ requires: { scopes: ['write'], tester_ok: false },
16
+ steps: [sender,
17
+ { n: 2, tool: 'get_effective_signature', args_example: { site_id: from(1, '0.id') }, note: 'Read the current saved default or brand-kit fallback; no write.' },
18
+ { n: 3, tool: 'generate_signature_from_brand_kit', args_example: { site_id: from(1, '0.id') }, note: 'Read a proposal; inspect omitted fields instead of inventing missing data.' },
19
+ { n: 4, tool: 'preview_signature', args_example: { site_id: from(1, '0.id'), html: from(3, 'html') }, note: 'Review the proposal and warnings. Fields can be supplied instead of HTML for simple edits.' },
20
+ { n: 5, tool: 'create_signature', args_example: { site_id: from(1, '0.id'), name: 'Brand signature', html: from(4, 'html'), is_default: true }, note: 'Only when asked to save. For an existing signature use update_signature with its id, html and is_default:true instead.' },
21
+ ],
22
+ gotchas: [projectRule, 'Only future AI-generated drafts use this default. No sending, existing campaign/template changes, postal address duplication, or bulk saving. Read and preview tools write nothing.'],
23
+ done_when: 'The preview is reviewed and, if requested, the selected site has the intended saved default.',
24
+ },
11
25
  {
12
26
  id: 'event-driven-billing-mail-end-to-end', title: 'Event-driven billing mail end to end',
13
27
  when_to_use: 'Build and verify event-driven billing, dunning, payment failed or trial ending transactional mail, then request human approval for activation.',
@@ -1586,8 +1586,8 @@ export function registerCampaignTools(server) {
1586
1586
  message: executed.grouped && executed.alreadySent
1587
1587
  ? `${executed.enrolled} ${payload.send_at ? `scheduled at ${payload.send_at}` : 'sent now'}; ${executed.alreadySent} skipped because they already received it.`
1588
1588
  : payload.send_at
1589
- ? `Scheduled: "${payload.subject}" goes to ${executed.enrolled} people at ${payload.send_at}.`
1590
- : `Sending: "${payload.subject}" is going to ${executed.enrolled} people now. Delivery happens over the next few minutes; check the campaign for results.`,
1589
+ ? `Scheduled: "${payload.subject}" goes to ${executed.enrolled} ${executed.enrolled === 1 ? 'person' : 'people'} at ${payload.send_at}.`
1590
+ : `Sending: "${payload.subject}" is going to ${executed.enrolled} ${executed.enrolled === 1 ? 'person' : 'people'} now. Delivery happens over the next few minutes; check the campaign for results.`,
1591
1591
  });
1592
1592
  }
1593
1593
  // ── Call 2: execute exactly the approved, frozen payload. Nothing else is trusted. ──
@@ -5,10 +5,61 @@ import { db } from '../db.js';
5
5
  import { getProjectId } from '../project.js';
6
6
  import { logAudit } from '../audit.js';
7
7
  import { dbToolError, deleteBlockedToolResult } from '../lib/db-error.js';
8
+ import { buildBrandKitSignature, getEffectiveSignature, previewSignature } from '@sendinel/domain';
8
9
  const txt = (text) => ({ content: [{ type: 'text', text }] });
9
10
  const j = (data) => txt(JSON.stringify(data, null, 2));
10
11
  const COLUMNS = 'id, site_id, name, html, is_default, created_at, updated_at';
11
12
  export function registerSignatureTools(server) {
13
+ const fields = z.object({
14
+ display_name: z.string().optional(), tagline: z.string().optional(), logo_url: z.string().optional(),
15
+ website_url: z.string().optional(), from_name: z.string().optional(), from_email: z.string().optional(),
16
+ accent_color: z.string().optional(), font_family: z.string().optional(),
17
+ });
18
+ const readHandler = (name) => async (input) => {
19
+ const projectId = getProjectId();
20
+ const client = db();
21
+ const scoped = typeof client.schema === 'function' ? client.schema('email') : client;
22
+ const { data: site, error: siteError } = await scoped.from('sites')
23
+ .select('id, name, from_name, from_email, website_url').eq('id', input.site_id).eq('project_id', projectId).maybeSingle();
24
+ if (siteError)
25
+ return j(dbToolError(siteError));
26
+ if (!site)
27
+ return j({ error: 'Site not found', code: 'NOT_FOUND' });
28
+ if (name === 'preview_signature' && (input.html !== undefined || input.fields !== undefined)) {
29
+ if (input.html !== undefined && input.fields !== undefined)
30
+ return j({ error: 'Supply html or fields, not both', code: 'validation_failed' });
31
+ return j(previewSignature(input));
32
+ }
33
+ const { data: project, error: projectError } = await scoped.from('projects').select('brand_settings').eq('id', projectId).maybeSingle();
34
+ if (projectError)
35
+ return j(dbToolError(projectError));
36
+ if (!project)
37
+ return j({ error: 'Project not found', code: 'NOT_FOUND' });
38
+ const brandInput = { site, brandSettings: project.brand_settings };
39
+ if (name === 'generate_signature_from_brand_kit')
40
+ return j(buildBrandKitSignature(brandInput));
41
+ const { data: savedDefault, error: signatureError } = await scoped.from('signature_blocks').select('html')
42
+ .eq('project_id', projectId).eq('site_id', input.site_id).eq('is_default', true).maybeSingle();
43
+ if (signatureError)
44
+ return j(dbToolError(signatureError));
45
+ const effective = getEffectiveSignature({ ...brandInput, savedDefault });
46
+ return j(name === 'preview_signature' ? { ...effective, ...previewSignature({ html: effective.html }) } : effective);
47
+ };
48
+ server.registerTool('get_effective_signature', {
49
+ title: 'Get Effective Signature', annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
50
+ description: 'Read the saved default signature or deterministic brand-kit fallback for future AI drafts. Writes nothing.',
51
+ inputSchema: { site_id: z.string().uuid() },
52
+ }, readHandler('get_effective_signature'));
53
+ server.registerTool('generate_signature_from_brand_kit', {
54
+ title: 'Generate Signature from Brand Kit', annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
55
+ description: 'Propose an email-safe signature from stored brand settings. Missing fields are omitted and listed. Writes nothing; save via create_signature or update_signature.',
56
+ inputSchema: { site_id: z.string().uuid() },
57
+ }, readHandler('generate_signature_from_brand_kit'));
58
+ server.registerTool('preview_signature', {
59
+ title: 'Preview Signature', annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
60
+ description: 'Preview a signature candidate (HTML or fields), or the effective signature when neither is supplied. Returns final HTML and email-safety warnings. Writes nothing.',
61
+ inputSchema: { site_id: z.string().uuid(), html: z.string().optional(), fields: fields.optional() },
62
+ }, readHandler('preview_signature'));
12
63
  server.registerTool('list_signatures', {
13
64
  title: 'List Signatures',
14
65
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.209",
3
+ "version": "1.0.211",
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",