@sendinel/mcp-server 1.0.133 → 1.0.134

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,4 @@
1
- import { checkProjectSendAllowlist } from "@sendinel/domain";
1
+ import { checkProjectSendAllowlist, stripActiveEmailContent } from "@sendinel/domain";
2
2
  import { Resend } from "resend";
3
3
  import { adminDb, db } from "../db.js";
4
4
  import { decrypt } from "../crypto.js";
@@ -251,6 +251,7 @@ export async function sendEmailForSite(projectId, siteId, args) {
251
251
  const refusal = await checkProjectSendAllowlist(adminDb(), projectId, args, "mcp");
252
252
  if (refusal)
253
253
  return refusal;
254
+ args = { ...args, ...(args.html !== undefined ? { html: stripActiveEmailContent(args.html).html } : {}) };
254
255
  const config = await resolveProviderConfig(projectId, siteId);
255
256
  switch (config.name) {
256
257
  case "resend":
@@ -2,7 +2,7 @@ import { resolveCampaignReentry, readCampaignPeriods, campaignPeriodFields } fro
2
2
  import { CAMPAIGN_BRANCH_CONDITIONS, campaignBranchConditionValue, isContactPropertyConditionList, STEP_PROPERTY_CONDITIONS_ERROR, validateStepPropertyRules } from "@sendinel/domain";
3
3
  import { z } from 'zod';
4
4
  import Anthropic from '@anthropic-ai/sdk';
5
- import { assertSendAccepted, providerSendError, sendError, 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, isSegmentRulesError, scopeSegmentRulesToSite, SegmentRulesError, validateSegmentRules, validateUnsubFooterTemplate, } from '@sendinel/domain';
5
+ import { stripActiveEmailContent, assertSendAccepted, providerSendError, sendError, 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, isSegmentRulesError, scopeSegmentRulesToSite, SegmentRulesError, validateSegmentRules, validateUnsubFooterTemplate, } from '@sendinel/domain';
6
6
  // Subpath import, not the barrel above -- reply-actions.ts uses node:crypto,
7
7
  // and MCP-server-side that's fine (this never runs in an Edge Function), but
8
8
  // keeping it off the shared barrel avoids re-creating the Edge-Function
@@ -1510,7 +1510,7 @@ export function registerCampaignTools(server) {
1510
1510
  });
1511
1511
  return j({
1512
1512
  ...approval,
1513
- rendered: { subject: content.subject, body_html: content.body_html, body_text: content.body_text },
1513
+ rendered: { subject: content.subject, body_html: stripActiveEmailContent(content.body_html).html, body_text: content.body_text },
1514
1514
  recipient_count: resolved.contactIds.length,
1515
1515
  audience: { label: segmentName, segment_id: segment_id ?? null, ...heldBackLines(resolved) },
1516
1516
  site: { id: site.id, name: site.name, from_email: site.from_email },
@@ -2461,6 +2461,8 @@ Do not include body_html in your response; Sendinel renders body_html from block
2461
2461
  }
2462
2462
  resolvedSubject = subject?.trim() || renderTemplateVars(template.subject_hint ?? '', renderVars, { brandTokens: brandVars });
2463
2463
  }
2464
+ if (resolvedHtml !== undefined)
2465
+ resolvedHtml = stripActiveEmailContent(resolvedHtml).html;
2464
2466
  if (!resolvedSubject?.trim())
2465
2467
  return txt('Error: subject is required');
2466
2468
  const payload = {
@@ -2653,6 +2655,8 @@ Do not include body_html in your response; Sendinel renders body_html from block
2653
2655
  }
2654
2656
  resolvedSubject = subject?.trim() || renderTemplateVars(template.subject_hint ?? '', renderVars, { brandTokens: brandVars });
2655
2657
  }
2658
+ if (resolvedHtml !== undefined)
2659
+ resolvedHtml = stripActiveEmailContent(resolvedHtml).html;
2656
2660
  if (!resolvedSubject?.trim())
2657
2661
  return txt('Error: subject is required');
2658
2662
  const { data: projectContext, error: projectContextError } = await adminDb()
@@ -2871,6 +2875,17 @@ Do not include body_html in your response; Sendinel renders body_html from block
2871
2875
  bodyText: body_text,
2872
2876
  brandSettings: projectRow?.brand_settings ?? null,
2873
2877
  });
2878
+ const active = stripActiveEmailContent(html).removed;
2879
+ if (active.includes('script'))
2880
+ result.warnings.push('The script tag is removed at send time.');
2881
+ if (active.includes('event-handler attribute'))
2882
+ result.warnings.push('Each on* event-handler attribute is removed at send time.');
2883
+ const schemes = active.filter(finding => finding.endsWith(' URL'));
2884
+ if (schemes.length)
2885
+ result.warnings.push(`Active links (${schemes.join(', ')}) are removed at send time.`);
2886
+ const tags = active.filter(finding => finding !== 'script' && finding !== 'event-handler attribute' && !finding.endsWith(' URL'));
2887
+ if (tags.length)
2888
+ result.warnings.push(`Other active tags (${tags.join(', ')}) are removed at send time.`);
2874
2889
  for (const issue of findContentIssues({ subject, body_html: html, body_text }, { sendingHosts, sourcedKeys })) {
2875
2890
  if (BLOCKING_CONTENT_ISSUE_KINDS.has(issue.kind))
2876
2891
  result.errors.push(`${issue.message} (${issue.excerpt})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.133",
3
+ "version": "1.0.134",
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",