@sendinel/mcp-server 1.0.171 → 1.0.172

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,5 +1,9 @@
1
1
  import type Anthropic from '@anthropic-ai/sdk';
2
2
  import { type ComposerBlock } from './composer-blocks.js';
3
+ /** Known unsupported targeting must fail before a model can invent a property rule. */
4
+ export declare function unsupportedAudienceReason(description: string): string | null;
5
+ export declare class AudienceNotUnderstoodError extends Error {
6
+ }
3
7
  export declare function parseAudienceRules(anthropic: Anthropic, description: string): Promise<Record<string, unknown>>;
4
8
  export type GeneratedEmail = {
5
9
  subject: string;
@@ -11,24 +11,39 @@ import { BLOCK_GENERATION_SCHEMA, blocksToHtml, blocksToText, normalizeComposerB
11
11
  function parseJsonResponse(raw) {
12
12
  return JSON.parse(raw.replace(/```json?\n?|```/g, '').trim());
13
13
  }
14
+ const EMAIL_DOMAIN_AUDIENCE_MESSAGE = 'Audiences by email address or domain are not supported. Create a segment using a tag or property rule, or pass segment_id for a saved segment.';
15
+ /** Known unsupported targeting must fail before a model can invent a property rule. */
16
+ export function unsupportedAudienceReason(description) {
17
+ return /\be-?mail[\s_-]+domains?\b|\be-?mail(?:\s+addresses?)?\s+(?:ends?\s+with|contains?|matches?|is\b|at\b)|@[a-z0-9](?:[a-z0-9.-]*\.)[a-z]{2,}\b/i.test(description)
18
+ ? EMAIL_DOMAIN_AUDIENCE_MESSAGE : null;
19
+ }
20
+ export class AudienceNotUnderstoodError extends Error {
21
+ }
14
22
  export async function parseAudienceRules(anthropic, description) {
23
+ const unsupported = unsupportedAudienceReason(description);
24
+ if (unsupported)
25
+ throw new AudienceNotUnderstoodError(unsupported);
15
26
  const resp = await anthropic.messages.create({
16
27
  model: getAnthropicModelId(),
17
28
  max_tokens: AI_MAX_TOKENS,
18
29
  ...outputConfig("low"),
19
- system: 'You translate natural language audience descriptions into a JSON filter rules object. Return ONLY valid JSON. Available keys: lifecycle_stage (lead|trial|customer|churned|win-back), payment (object with plan, mrr_cents, ltv_cents, subscription_status, trial_ends_at, churned_at, paid_at), min_score, max_score, source, subscribed_after, subscribed_before, has_opened, has_clicked, tag, custom_property, exclude_suppressed. Example: {"payment":{"plan":"pro","trial_ends_at":{"within_days":7}},"lifecycle_stage":"trial"}. Today is ' + new Date().toISOString().slice(0, 10) + '.',
30
+ system: 'You translate natural language audience descriptions into a JSON filter rules object. Return ONLY valid JSON. Available keys: match_all, lifecycle_stage (lead|trial|customer|churned|win-back), payment (object with plan, mrr_cents, ltv_cents, subscription_status, trial_ends_at, churned_at, paid_at), min_score, max_score, source, subscribed_after, subscribed_before, has_opened, has_clicked, tag, custom_property (a string like "plan=pro"), exclude_suppressed. Everyone, all contacts, all subscribers or all means {"match_all":true}. There is no email address or email-domain rule. Never invent keys or use custom_property for email targeting; return {"error":"' + EMAIL_DOMAIN_AUDIENCE_MESSAGE + '"} instead. For any other unsupported audience return an error object explaining why. Example: {"payment":{"plan":"pro","trial_ends_at":{"within_days":7}},"lifecycle_stage":"trial"}. Today is ' + new Date().toISOString().slice(0, 10) + '.',
20
31
  messages: [{ role: 'user', content: description }],
21
32
  });
22
33
  const text = anthropicCheckedText(resp);
34
+ let rules;
23
35
  try {
24
- const rules = parseJsonResponse(text);
36
+ rules = parseJsonResponse(text);
25
37
  if (!rules || typeof rules !== 'object' || Array.isArray(rules))
26
38
  throw new Error('not an object');
27
- return rules;
28
39
  }
29
40
  catch {
30
- throw new Error(`Could not understand the audience "${description}". Try a saved segment instead.`);
41
+ throw new AudienceNotUnderstoodError(`Could not understand the audience "${description}". Try a saved segment instead.`);
31
42
  }
43
+ if ('error' in rules)
44
+ throw new AudienceNotUnderstoodError(typeof rules.error === 'string'
45
+ ? rules.error : 'This audience is not supported. Try a saved segment instead.');
46
+ return rules;
32
47
  }
33
48
  export async function generateEmailFromBrief(anthropic, projectId, siteId, brief) {
34
49
  const [{ data: site, error: siteError }, { data: project, error: projectError }] = await Promise.all([
@@ -4,7 +4,7 @@ import { resolveCampaignReentry, readCampaignPeriods, campaignPeriodFields } fro
4
4
  import { CAMPAIGN_BRANCH_CONDITIONS, campaignBranchConditionValue, isContactPropertyConditionList, STEP_PROPERTY_CONDITIONS_ERROR, validateStepPropertyRules } from "@sendinel/domain";
5
5
  import { z } from 'zod';
6
6
  import Anthropic from '@anthropic-ai/sdk';
7
- 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';
7
+ 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, SEGMENT_MATCH_ALL, } from '@sendinel/domain';
8
8
  // Subpath import, not the barrel above -- reply-actions.ts uses node:crypto,
9
9
  // and MCP-server-side that's fine (this never runs in an Edge Function), but
10
10
  // keeping it off the shared barrel avoids re-creating the Edge-Function
@@ -22,7 +22,7 @@ import { fireWebhookEvent } from '../lib/webhook-events.js';
22
22
  import { approvalNext, approvalExcerpt, assertChannelCanExecute, currentChannelContext, requestApproval, checkApproval, withApproval } from '../lib/action-approvals.js';
23
23
  import { sandboxAudience, broadcastApprovalNext } from '../lib/sandbox-audience.js';
24
24
  import { resolveSegmentContactIds, resolveSendAudience } from '../lib/send-audience.js';
25
- import { generateEmailFromBrief, parseAudienceRules } from '../lib/email-generation.js';
25
+ import { AudienceNotUnderstoodError, generateEmailFromBrief, parseAudienceRules, unsupportedAudienceReason } from '../lib/email-generation.js';
26
26
  import { getAnthropicModelId } from '../anthropic-model.js';
27
27
  import { AI_MAX_TOKENS, outputConfig, anthropicCheckedText } from '@sendinel/domain';
28
28
  import { getScheduleWindowBounds, groupScheduleByCampaign, parseFutureDate } from '../lib/schedule.js';
@@ -1580,8 +1580,12 @@ export function registerCampaignTools(server) {
1580
1580
  sites: (sites ?? []).map((s) => ({ id: s.id, name: s.name, from_email: s.from_email })),
1581
1581
  });
1582
1582
  }
1583
+ const everyone = audience !== undefined && ['everyone', 'all contacts', 'all subscribers', 'all'].includes(audience.trim().toLowerCase().replace(/[.!?,;:]+$/, '').trim());
1584
+ const unsupported = audience ? unsupportedAudienceReason(audience) : null;
1585
+ if (unsupported)
1586
+ return j({ error: unsupported, code: 'AUDIENCE_NOT_UNDERSTOOD' });
1583
1587
  const apiKey = process.env.ANTHROPIC_API_KEY;
1584
- if ((audience || brief) && !apiKey)
1588
+ if (((audience && !everyone) || brief) && !apiKey)
1585
1589
  return j({ error: 'ANTHROPIC_API_KEY not configured', code: 'AI_NOT_CONFIGURED' });
1586
1590
  const anthropic = apiKey ? new Anthropic({ apiKey }) : null;
1587
1591
  let rules;
@@ -1600,12 +1604,17 @@ export function registerCampaignTools(server) {
1600
1604
  rules = scopeSegmentRulesToSite(segment.rules, segment.site_id);
1601
1605
  segmentName = segment.name;
1602
1606
  }
1607
+ else if (everyone) {
1608
+ rules = { ...SEGMENT_MATCH_ALL };
1609
+ segmentName = audience;
1610
+ }
1603
1611
  else {
1604
1612
  try {
1605
1613
  rules = await parseAudienceRules(anthropic, audience);
1606
1614
  }
1607
1615
  catch (error) {
1608
- return j({ error: error instanceof Error ? dbToolError(error).error : String(error), code: 'AUDIENCE_NOT_UNDERSTOOD' });
1616
+ return j({ error: error instanceof AudienceNotUnderstoodError ? error.message
1617
+ : error instanceof Error ? dbToolError(error).error : String(error), code: 'AUDIENCE_NOT_UNDERSTOOD' });
1609
1618
  }
1610
1619
  segmentName = audience;
1611
1620
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.171",
3
+ "version": "1.0.172",
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",