@sendinel/mcp-server 1.0.115 → 1.0.116

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 { applySegmentRules, buildSegmentBaseQuery, chunkIds, evaluateSegmentRules, normalizeSegmentRulesToTree, readAllRows, } from '@sendinel/domain';
1
+ import { applySegmentRules, buildSegmentBaseQuery, chunkIds, evaluateSegmentRules, hydrateEmailSendsContacts, normalizeSegmentRulesToTree, readAllRows, } from '@sendinel/domain';
2
2
  import { db } from '../db.js';
3
3
  async function loadFieldTypes(projectId) {
4
4
  const { data, error } = await db()
@@ -30,7 +30,8 @@ export async function resolveSegmentContactIds(projectId, rules) {
30
30
  if (siteContactIds && siteContactIds.length === 0)
31
31
  return [];
32
32
  const data = await readAllRows(filtered);
33
- const contacts = data.filter((contact) => evaluateSegmentRules(contact, tree, fieldTypes));
33
+ const hydrated = await hydrateEmailSendsContacts(db(), projectId, data, tree);
34
+ const contacts = hydrated.filter((contact) => evaluateSegmentRules(contact, tree, fieldTypes));
34
35
  if (tree.exclude_suppressed === false || contacts.length === 0)
35
36
  return contacts.map((c) => String(c.id));
36
37
  const suppressed = await readAllRows(db().from('suppressions').select('id, email').eq('project_id', projectId));
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { applySegmentRules, buildSegmentBaseQuery, evaluateSegmentRules, isSegmentRulesError, normalizeSegmentRulesToTree, readAllRows, } from '@sendinel/domain';
2
+ import { applySegmentRules, buildSegmentBaseQuery, evaluateSegmentRules, hydrateEmailSendsContacts, isSegmentRulesError, normalizeSegmentRulesToTree, readAllRows, } from '@sendinel/domain';
3
3
  import { db } from '../db.js';
4
4
  import { getProjectId } from '../project.js';
5
5
  import { logAudit } from '../audit.js';
@@ -39,7 +39,8 @@ async function getSegmentContactIds(rules) {
39
39
  return [];
40
40
  // Paged and loud (#3674): an unpaged read stopped at 1,000 rows, and an error read as "no one".
41
41
  const data = await readAllRows(filtered);
42
- const contacts = data.filter((contact) => evaluateSegmentRules(contact, tree, fieldTypes));
42
+ const hydrated = await hydrateEmailSendsContacts(db(), projectId, data, tree);
43
+ const contacts = hydrated.filter((contact) => evaluateSegmentRules(contact, tree, fieldTypes));
43
44
  let ids = contacts.map((contact) => String(contact.id));
44
45
  if (tree.exclude_suppressed !== false && ids.length > 0) {
45
46
  const suppressed = await readAllRows(db().from('suppressions').select('id, email').eq('project_id', projectId));
@@ -5,7 +5,7 @@ import { db } from '../db.js';
5
5
  import { getProjectId } from '../project.js';
6
6
  import { getAnthropicModelId } from '../anthropic-model.js';
7
7
  import { assertUsableStopReason } from '@sendinel/domain';
8
- import { applySegmentRules, buildSegmentBaseQuery, evaluateSegmentRules, isSegmentRulesError, normalizeSegmentRulesToTree, prepareSegmentRulesForStorage, readAllRows, } from '@sendinel/domain';
8
+ import { applySegmentRules, buildSegmentBaseQuery, evaluateSegmentRules, hydrateEmailSendsContacts, isSegmentRulesError, normalizeSegmentRulesToTree, prepareSegmentRulesForStorage, readAllRows, } from '@sendinel/domain';
9
9
  async function loadFieldTypes(projectId) {
10
10
  const client = db();
11
11
  const scoped = typeof client.schema === 'function' ? client.schema('email') : client;
@@ -41,7 +41,8 @@ async function getSegmentContactIds(rules) {
41
41
  return [];
42
42
  // Paged and loud (#3674): an unpaged read stopped at 1,000 rows, and an error read as "no one".
43
43
  const data = await readAllRows(filtered);
44
- const contacts = data.filter((contact) => evaluateSegmentRules(contact, tree, fieldTypes));
44
+ const hydrated = await hydrateEmailSendsContacts(db(), projectId, data, tree);
45
+ const contacts = hydrated.filter((contact) => evaluateSegmentRules(contact, tree, fieldTypes));
45
46
  let ids = contacts.map((c) => String(c.id));
46
47
  if (tree.exclude_suppressed !== false && ids.length > 0) {
47
48
  const suppressed = await readAllRows(db().from('suppressions').select('id, email').eq('project_id', projectId));
@@ -76,7 +77,7 @@ export function registerSegmentTools(server) {
76
77
  inputSchema: {
77
78
  name: z.string().min(1).describe('Segment name (unique per project)'),
78
79
  description: z.string().optional(),
79
- rules: z.record(z.string(), z.unknown()).describe('Segment rule tree: {op:"AND"|"OR", conditions:[{field, operator, value}]} or legacy flat rules. Supported fields include lifecycle_stage, engagement_score, source, created_at, last_opened_at, last_clicked_at, introduced_by_subscriber_id, tags, custom_properties.<key>, payment.<key>. tag may be a plain string (one tag) or {values: string[], mode: "any"|"all"} to match several tags at once.'),
80
+ rules: z.record(z.string(), z.unknown()).describe('Segment rule tree: {op:"AND"|"OR", conditions:[{field, operator, value}]} or legacy flat rules. Supported fields include lifecycle_stage, engagement_score, source, created_at, last_opened_at, last_clicked_at, introduced_by_subscriber_id, tags, custom_properties.<key>, payment.<key>, email_sends (eq only, value: {min_count: positive integer, within_days: positive number}; at most one per segment). tag may be a plain string (one tag) or {values: string[], mode: "any"|"all"} to match several tags at once.'),
80
81
  },
81
82
  }, async ({ name, description, rules }) => {
82
83
  let storedRules;
@@ -281,7 +282,7 @@ export function registerSegmentTools(server) {
281
282
  const resp = await anthropic.messages.create({
282
283
  model: getAnthropicModelId(),
283
284
  max_tokens: 4096,
284
- system: 'You translate natural language audience descriptions into a JSON segment rule tree. Return ONLY valid JSON. Format: {"op":"AND"|"OR","conditions":[conditionOrGroup],"exclude_suppressed":true}. Condition format: {"field": string, "operator":"eq|neq|gt|gte|lt|lte|contains|exists|not_exists|after|before|on_or_after|on_or_before","value": any}. Supported fields: lifecycle_stage, engagement_score, source, created_at, last_opened_at, last_clicked_at, introduced_by_subscriber_id, tags, custom_properties.<key>, payment.plan, payment.mrr_cents, payment.ltv_cents, payment.subscription_status, payment.trial_ends_at, payment.churned_at, payment.paid_at. Example: {"op":"AND","conditions":[{"field":"lifecycle_stage","operator":"eq","value":"trial"},{"field":"payment.trial_ends_at","operator":"on_or_before","value":"' + new Date(Date.now() + 7 * 86400000).toISOString() + '"}]}. Today is ' + new Date().toISOString().slice(0, 10) + '.',
285
+ system: 'You translate natural language audience descriptions into a JSON segment rule tree. Return ONLY valid JSON. Format: {"op":"AND"|"OR","conditions":[conditionOrGroup],"exclude_suppressed":true}. Condition format: {"field": string, "operator":"eq|neq|gt|gte|lt|lte|contains|exists|not_exists|after|before|on_or_after|on_or_before","value": any}. Supported fields: lifecycle_stage, engagement_score, source, created_at, last_opened_at, last_clicked_at, introduced_by_subscriber_id, tags, custom_properties.<key>, payment.plan, payment.mrr_cents, payment.ltv_cents, payment.subscription_status, payment.trial_ends_at, payment.churned_at, payment.paid_at, email_sends. email_sends uses eq only and value {"min_count":4,"within_days":30}; both required, at most one per segment. It counts email_log rows with non-null sent_at in the inclusive elapsed-day window through now, regardless of current status. No observed sends = zero; failed history reads are errors. Example: {"op":"AND","conditions":[{"field":"lifecycle_stage","operator":"eq","value":"trial"},{"field":"payment.trial_ends_at","operator":"on_or_before","value":"' + new Date(Date.now() + 7 * 86400000).toISOString() + '"}]}. Today is ' + new Date().toISOString().slice(0, 10) + '.',
285
286
  messages: [{ role: 'user', content: description }],
286
287
  });
287
288
  assertUsableStopReason(resp.stop_reason);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.115",
3
+ "version": "1.0.116",
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",