@sendinel/mcp-server 1.0.92 → 1.0.94

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 35 tool groups and 337 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.
15
+ Sendinel currently exposes 35 tool groups and 338 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.
16
16
 
17
17
  | Group | Count | Purpose |
18
18
  |-------|-------|---------|
@@ -23,7 +23,7 @@ Sendinel currently exposes 35 tool groups and 337 tools (gateway mode exposes 4
23
23
  | `analytics` | 15 | Stats, portfolio analytics, project switching, domain health, engagement, deliverability, revenue analytics |
24
24
  | `approvals` | 2 | Poll and list risk-tiered action approvals |
25
25
  | `automations` | 7 | Automation triggers, previews, and response-gated escalation ladders |
26
- | `brand_kit` | 5 | Brand Core text import, media library upload/URL ingest, asset-context reads |
26
+ | `brand_kit` | 6 | Brand Core text import, media library upload/URL ingest, asset-context reads, project logo selection |
27
27
  | `campaigns` | 43 | Full campaign lifecycle: create, edit, approve, launch, enroll, schedule, sequence templates, convert imported campaigns |
28
28
  | `cloud-import` | 6 | Cloud storage import |
29
29
  | `compound` | 4 | Higher-level campaign/template/site setup and delivery diagnosis workflows |
@@ -100,6 +100,7 @@ export const APPROVAL_EXEMPT = {
100
100
  run_list_hygiene: "suppresses invalid or bouncing addresses, which only reduces who receives mail",
101
101
  run_portfolio_analysis: "writes an internal analysis report only",
102
102
  set_active_project: "switches which project this connection targets; no data written",
103
+ set_brand_logo: "copies an approved image to email storage and updates reversible project branding; sends nothing",
103
104
  set_campaign_goal: "sets a goal definition on a campaign; sends nothing",
104
105
  set_subscriber_tags: "edits tags on one contact; tag triggers go through enrollment rules the owner configured. FOLLOW-UP: confirm tag-triggered automations are covered",
105
106
  set_white_label_settings: "edits branding settings; reversible and sends nothing",
package/build/server.js CHANGED
@@ -135,7 +135,7 @@ const WRITE_TOOLS = new Set([
135
135
  'create_signature', 'update_signature',
136
136
  'mark_ai_work_item_seen', 'snooze_ai_work_item', 'dismiss_ai_work_item',
137
137
  'submit_ai_context_event',
138
- 'import_brand_kit_text', 'rehost_brand_asset', 'ingest_brand_assets_by_url', 'upload_brand_asset',
138
+ 'import_brand_kit_text', 'rehost_brand_asset', 'set_brand_logo', 'ingest_brand_assets_by_url', 'upload_brand_asset',
139
139
  'create_newsletter', 'update_newsletter', 'create_newsletter_issue', 'schedule_newsletter_issue',
140
140
  'send_newsletter_issue', 'subscribe_to_newsletter', 'unsubscribe_from_newsletter',
141
141
  'retry_send_queue_rows', 'cancel_send_queue_rows',
@@ -63,6 +63,15 @@ export function registerBrandKitTools(server) {
63
63
  const result = await callBrandKitApi(`/api/v1/brand-kit/assets/${asset_id}/rehost`, { purpose, platforms });
64
64
  return result.ok ? j(result.data) : j({ error: result.error });
65
65
  });
66
+ server.registerTool('set_brand_logo', {
67
+ title: 'Set Project Brand Logo',
68
+ description: 'Use a confirmed PNG, JPEG, GIF or WebP brand asset as this project\'s email logo. Copies it to stable public email storage, preserves other brand settings, and reports site logo overrides without changing them.',
69
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
70
+ inputSchema: { asset_id: z.string().uuid() },
71
+ }, async ({ asset_id }) => {
72
+ const result = await callBrandKitApi(`/api/v1/brand-kit/assets/${asset_id}/use-as-logo`, {});
73
+ return result.ok ? j(result.data) : { ...j({ error: result.error }), isError: true };
74
+ });
66
75
  server.registerTool('list_brand_assets', {
67
76
  title: 'List Brand Assets',
68
77
  annotations: {
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import Anthropic from '@anthropic-ai/sdk';
3
- import { assertSendAccepted, SendRefusedError, BLOCKING_CONTENT_ISSUE_KINDS, computeSendContentKey, findContentIssues, sendingHostOf, NAME_FALLBACKS, ONE_SEND_MATCH_STATUSES, ONE_SEND_SENT_STATUSES, ONE_SEND_WINDOW_DAYS, normalizeTriggerConfig, renderTemplateString as renderTemplateVars, resolveBrandTokens, readAllRows, } from '@sendinel/domain';
3
+ import { assertSendAccepted, SendRefusedError, BLOCKING_CONTENT_ISSUE_KINDS, computeSendContentKey, findContentIssues, sendingHostOf, NAME_FALLBACKS, ONE_SEND_MATCH_STATUSES, ONE_SEND_SENT_STATUSES, ONE_SEND_WINDOW_DAYS, normalizeTriggerConfig, renderTemplateString as renderTemplateVars, resolveBrandTokens, brandWarnings, readAllRows, } from '@sendinel/domain';
4
4
  // Subpath import, not the barrel above -- reply-actions.ts uses node:crypto,
5
5
  // and MCP-server-side that's fine (this never runs in an Edge Function), but
6
6
  // keeping it off the shared barrel avoids re-creating the Edge-Function
@@ -2342,7 +2342,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
2342
2342
  // Site verification
2343
2343
  const { data: site } = await db()
2344
2344
  .from('sites')
2345
- .select('from_email, from_name, name, brand_voice')
2345
+ .select('from_email, from_name, name, brand_voice, physical_address')
2346
2346
  .eq('id', site_id)
2347
2347
  .eq('project_id', projectId)
2348
2348
  .maybeSingle();
@@ -2351,6 +2351,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
2351
2351
  let resolvedSubject = subject;
2352
2352
  let resolvedHtml = html;
2353
2353
  let resolvedText = text;
2354
+ let brandWarningCodes = [];
2354
2355
  if (template_id) {
2355
2356
  const { data: template, error: templateError } = await db()
2356
2357
  .from('campaign_templates')
@@ -2366,7 +2367,10 @@ Do not include body_html in your response; Sendinel renders body_html from block
2366
2367
  const projectBrand = (project?.brand_settings ?? null);
2367
2368
  const siteBrand = (site.brand_voice ?? null);
2368
2369
  const mergedBrand = projectBrand || siteBrand ? { ...(projectBrand ?? {}), ...(siteBrand ?? {}) } : null;
2369
- const brandVars = resolveBrandTokens(mergedBrand, site.name || undefined);
2370
+ const brandVars = resolveBrandTokens(mergedBrand, site.name || undefined, { address: site.physical_address ?? undefined });
2371
+ brandWarningCodes = brandWarnings(mergedBrand);
2372
+ if (brandWarningCodes.length)
2373
+ console.warn('MCP send brand warnings', { projectId, site_id, warnings: brandWarningCodes });
2370
2374
  const vars = template_vars ?? {};
2371
2375
  const renderVars = { ...brandVars, ...vars };
2372
2376
  const subjectFallingBack = !subject?.trim();
@@ -2378,6 +2382,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
2378
2382
  const satisfied = new Set([
2379
2383
  ...Object.keys(vars),
2380
2384
  'brand_name', 'brand_logo_url', 'brand_bg_color', 'brand_text_color', 'brand_accent_color',
2385
+ 'brand_logo_html', 'brand_font', 'brand_address',
2381
2386
  ...Object.keys(NAME_FALLBACKS),
2382
2387
  ]);
2383
2388
  const missing = requiredVars.filter((name) => !satisfied.has(name));
@@ -2385,14 +2390,14 @@ Do not include body_html in your response; Sendinel renders body_html from block
2385
2390
  return txt(`Error: Missing required template variable(s): ${missing.join(', ')}`);
2386
2391
  }
2387
2392
  if (template.format === 'text') {
2388
- resolvedText = renderTemplateVars(template.body_text ?? '', renderVars);
2393
+ resolvedText = renderTemplateVars(template.body_text ?? '', renderVars, { brandTokens: brandVars });
2389
2394
  resolvedHtml = undefined;
2390
2395
  }
2391
2396
  else {
2392
- resolvedHtml = renderTemplateVars(template.body_html ?? '', renderVars);
2397
+ resolvedHtml = renderTemplateVars(template.body_html ?? '', renderVars, { brandTokens: brandVars });
2393
2398
  resolvedText = text ?? stripHtml(resolvedHtml);
2394
2399
  }
2395
- resolvedSubject = subject?.trim() || renderTemplateVars(template.subject_hint ?? '', renderVars);
2400
+ resolvedSubject = subject?.trim() || renderTemplateVars(template.subject_hint ?? '', renderVars, { brandTokens: brandVars });
2396
2401
  }
2397
2402
  if (!resolvedSubject?.trim())
2398
2403
  return txt('Error: subject is required');
@@ -2432,6 +2437,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
2432
2437
  }).select('id').single();
2433
2438
  return j({
2434
2439
  sent: true,
2440
+ ...(brandWarningCodes.length ? { warnings: brandWarningCodes } : {}),
2435
2441
  id: logRow?.id,
2436
2442
  to,
2437
2443
  provider: sendResult.provider,
@@ -2525,7 +2531,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
2525
2531
  // Site verification
2526
2532
  const { data: site } = await db()
2527
2533
  .from('sites')
2528
- .select('from_email, from_name, name, brand_voice')
2534
+ .select('from_email, from_name, name, brand_voice, physical_address')
2529
2535
  .eq('id', site_id)
2530
2536
  .eq('project_id', projectId)
2531
2537
  .maybeSingle();
@@ -2534,6 +2540,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
2534
2540
  let resolvedSubject = subject;
2535
2541
  let resolvedHtml = html;
2536
2542
  let resolvedText = text;
2543
+ let brandWarningCodes = [];
2537
2544
  if (template_id) {
2538
2545
  const { data: template, error: templateError } = await db()
2539
2546
  .from('campaign_templates')
@@ -2549,7 +2556,10 @@ Do not include body_html in your response; Sendinel renders body_html from block
2549
2556
  const projectBrand = (project?.brand_settings ?? null);
2550
2557
  const siteBrand = (site.brand_voice ?? null);
2551
2558
  const mergedBrand = projectBrand || siteBrand ? { ...(projectBrand ?? {}), ...(siteBrand ?? {}) } : null;
2552
- const brandVars = resolveBrandTokens(mergedBrand, site.name || undefined);
2559
+ const brandVars = resolveBrandTokens(mergedBrand, site.name || undefined, { address: site.physical_address ?? undefined });
2560
+ brandWarningCodes = brandWarnings(mergedBrand);
2561
+ if (brandWarningCodes.length)
2562
+ console.warn('MCP send brand warnings', { projectId, site_id, warnings: brandWarningCodes });
2553
2563
  const vars = template_vars ?? {};
2554
2564
  const renderVars = { ...brandVars, ...vars };
2555
2565
  const subjectFallingBack = !subject?.trim();
@@ -2561,6 +2571,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
2561
2571
  const satisfied = new Set([
2562
2572
  ...Object.keys(vars),
2563
2573
  'brand_name', 'brand_logo_url', 'brand_bg_color', 'brand_text_color', 'brand_accent_color',
2574
+ 'brand_logo_html', 'brand_font', 'brand_address',
2564
2575
  ...Object.keys(NAME_FALLBACKS),
2565
2576
  ]);
2566
2577
  const missing = requiredVars.filter((name) => !satisfied.has(name));
@@ -2568,14 +2579,14 @@ Do not include body_html in your response; Sendinel renders body_html from block
2568
2579
  return txt(`Error: Missing required template variable(s): ${missing.join(', ')}`);
2569
2580
  }
2570
2581
  if (template.format === 'text') {
2571
- resolvedText = renderTemplateVars(template.body_text ?? '', renderVars);
2582
+ resolvedText = renderTemplateVars(template.body_text ?? '', renderVars, { brandTokens: brandVars });
2572
2583
  resolvedHtml = undefined;
2573
2584
  }
2574
2585
  else {
2575
- resolvedHtml = renderTemplateVars(template.body_html ?? '', renderVars);
2586
+ resolvedHtml = renderTemplateVars(template.body_html ?? '', renderVars, { brandTokens: brandVars });
2576
2587
  resolvedText = text ?? stripHtml(resolvedHtml);
2577
2588
  }
2578
- resolvedSubject = subject?.trim() || renderTemplateVars(template.subject_hint ?? '', renderVars);
2589
+ resolvedSubject = subject?.trim() || renderTemplateVars(template.subject_hint ?? '', renderVars, { brandTokens: brandVars });
2579
2590
  }
2580
2591
  if (!resolvedSubject?.trim())
2581
2592
  return txt('Error: subject is required');
@@ -2666,6 +2677,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
2666
2677
  }).eq('id', logRow.id);
2667
2678
  return j({
2668
2679
  sent: true,
2680
+ ...(brandWarningCodes.length ? { warnings: brandWarningCodes } : {}),
2669
2681
  id: logRow.id,
2670
2682
  reply_action_id: minted.id,
2671
2683
  correlation_id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.92",
3
+ "version": "1.0.94",
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",