@sendinel/mcp-server 1.0.151 → 1.0.153

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.
@@ -13,6 +13,7 @@ type CampaignSetRow = {
13
13
  recipient_count?: number | null;
14
14
  audience?: Record<string, unknown> | null;
15
15
  audience_error?: string;
16
+ code?: string;
16
17
  };
17
18
  type StepRow = {
18
19
  campaign_id: string;
@@ -41,6 +42,7 @@ export declare function previewOf(campaigns: CampaignSetRow[], steps: StepRow[])
41
42
  recipient_count?: number | null | undefined;
42
43
  audience?: Record<string, unknown> | null | undefined;
43
44
  audience_error?: string | undefined;
45
+ code?: string | undefined;
44
46
  activation?: string | undefined;
45
47
  emails: {
46
48
  step_order: number;
@@ -77,7 +77,7 @@ export function previewOf(campaigns, steps) {
77
77
  ...(c.type === 'broadcast' ? {
78
78
  recipient_count: c.recipient_count ?? null,
79
79
  audience: c.audience ?? null,
80
- ...(c.audience_error ? { audience_error: c.audience_error } : {}),
80
+ ...(c.audience_error ? { audience_error: c.audience_error, code: c.code } : {}),
81
81
  activation: BROADCAST_ACTIVATION,
82
82
  } : {}),
83
83
  emails: steps
@@ -114,8 +114,10 @@ export async function loadSet(projectId, campaignIds) {
114
114
  Object.assign(campaign, { recipient_count, audience });
115
115
  }
116
116
  catch (error) {
117
+ console.error("[campaign-sets] Audience lookup failed:", error);
118
+ const safe = dbToolError(error, "Unable to load campaign audience");
117
119
  Object.assign(campaign, { recipient_count: null, audience: null,
118
- audience_error: error instanceof Error ? error.message : String(error) });
120
+ audience_error: safe.error, code: safe.code });
119
121
  }
120
122
  }
121
123
  return { campaigns: loaded, steps: (steps ?? []) };
@@ -2842,11 +2842,12 @@ Do not include body_html in your response; Sendinel renders body_html from block
2842
2842
  }
2843
2843
  catch (err) {
2844
2844
  await db().from('email_log').update({ status: 'failed' }).eq('id', logRow.id);
2845
+ console.error("[campaigns] Reply action mint failed:", err);
2845
2846
  const errMsg = err instanceof Error ? err.message : String(err);
2846
2847
  if (errMsg.includes('23505') || errMsg.includes('uidx_reply_actions_pending_correlation') || errMsg.includes('already pending')) {
2847
2848
  return txt('An awaiting-reply send for this correlation_id is already pending.');
2848
2849
  }
2849
- return txt(`Error: Failed to mint reply action — ${errMsg}`);
2850
+ return toolError(dbToolError(err, "Failed to mint reply action"));
2850
2851
  }
2851
2852
  const payload = {
2852
2853
  from: `${site.from_name} <${site.from_email}>`,
@@ -6,7 +6,7 @@ import { getProjectId } from '../project.js';
6
6
  import { buildVoicePrompt, deriveTemplateVariables } from '../lib/voice.js';
7
7
  import { parseModelJson, getAnthropicFastModelId } from '../anthropic-model.js';
8
8
  import { BLOCK_GENERATION_SCHEMA, blocksToHtml, blocksToText, normalizeComposerBlocks } from '../lib/composer-blocks.js';
9
- import { dbToolError } from '../lib/db-error.js';
9
+ import { dbToolError, toolError } from '../lib/db-error.js';
10
10
  // Haiku 5.5 (fast role) thinks before answering; thinking counts toward max_tokens (#4193).
11
11
  const FAST_MIN_MAX_TOKENS = 8000;
12
12
  const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
@@ -221,7 +221,8 @@ Respond with JSON: { "subject": "...", "blocks": [...], "body_text": "..." }`,
221
221
  };
222
222
  }
223
223
  catch (error) {
224
- return j({ error: `Generation failed: ${error instanceof Error ? error.message : String(error)}` });
224
+ console.error("[compound] Generation failed:", error);
225
+ return toolError(dbToolError(error, "Generation failed: unable to generate template"));
225
226
  }
226
227
  const { data: template, error } = await db().from('campaign_templates')
227
228
  .insert({
@@ -2,7 +2,7 @@ import { normalizeTagInput, tagInputWarnings } from '../lib/tags.js';
2
2
  import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import { CONTACT_ENRICHMENT_PROVIDERS } from '@desler/models';
4
4
  import { createContactEnrichmentAdapters, enrichContactFromConnections, } from '@desler/models/contact-enrichment';
5
- import { buildPreviewAttentionEnvelope, applySegmentRules, isSegmentRulesError, ContactPropertyAnomalyLogger, normalizeContactProperties, getAttentionDecision, getContactNeighborhood, getScheduledTouches, findContactDuplicateGroups, readAllPages, readAllRows, chunkIds, } from '@sendinel/domain';
5
+ import { buildPreviewAttentionEnvelope, applySegmentRules, isSegmentRulesError, ContactPropertyAnomalyLogger, normalizeContactProperties, validateContactPropertyPatch, getAttentionDecision, getContactNeighborhood, getScheduledTouches, findContactDuplicateGroups, readAllPages, readAllRows, chunkIds, } from '@sendinel/domain';
6
6
  import { z } from 'zod';
7
7
  import { callSendinelV1 } from '../lib/v1-client.js';
8
8
  import { db, projectClientForWriteback } from '../db.js';
@@ -45,22 +45,6 @@ const CUSTOM_FIELD_COLUMNS = 'id, key, label, type, required, options, display_o
45
45
  async function registeredCustomFields(projectId) {
46
46
  return readAllRows(db().from('custom_field_definitions').select(CUSTOM_FIELD_COLUMNS).eq('project_id', projectId));
47
47
  }
48
- function keyDistance(left, right) {
49
- let row = Array.from({ length: right.length + 1 }, (_, index) => index);
50
- for (let i = 0; i < left.length; i++) {
51
- const next = [i + 1];
52
- for (let j = 0; j < right.length; j++)
53
- next.push(Math.min(next[j] + 1, row[j + 1] + 1, row[j] + (left[i] === right[j] ? 0 : 1)));
54
- row = next;
55
- }
56
- return row[right.length];
57
- }
58
- function unknownCustomPropertyWarnings(patch, fields) {
59
- return Object.keys(patch).filter(key => !Object.hasOwn(fields, key)).map(key => {
60
- const near = Object.keys(fields).filter(known => keyDistance(key, known) <= 2);
61
- return `Unknown custom property "${key}"${near.length ? `; did you mean ${near.map(value => `"${value}"`).join(' or ')}?` : '; no registered definition'}`;
62
- });
63
- }
64
48
  const SUBSCRIBER_VIEW_MAX_NAME_LENGTH = 100;
65
49
  const SUBSCRIBER_VIEW_MAX_COLUMNS = 150;
66
50
  const SUBSCRIBER_VIEW_KEY_RE = /^[a-zA-Z0-9_.:-]{1,160}$/;
@@ -182,7 +166,8 @@ async function writeBackMcpContact(projectId, contact, patch) {
182
166
  await customer.schema('email').from('subscribers').update(update).eq('id', target.rowId);
183
167
  }
184
168
  catch (error) {
185
- logAudit({ action: 'byod_writeback_failed', resourceType: 'contact', resourceId: contact?.id, details: { email: contact?.email, error: error instanceof Error ? error.message : String(error) } });
169
+ console.error('[contacts] BYOD writeback failed:', error);
170
+ logAudit({ action: 'byod_writeback_failed', resourceType: 'contact', resourceId: contact?.id, details: { email: contact?.email, ...dbToolError(error, 'Contact writeback failed') } });
186
171
  }
187
172
  }
188
173
  function daysAgo(days) {
@@ -1835,21 +1820,10 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
1835
1820
  return j({ error: 'Custom field definitions unavailable; update not performed', code: 'INTERNAL_ERROR' });
1836
1821
  }
1837
1822
  fields = Object.fromEntries(definitions.map(field => [field.key, field]));
1838
- const values = Object.fromEntries(Object.entries(custom_properties).filter(([, value]) => value !== null));
1839
- const invalidShape = Object.entries(values).find(([key, value]) => {
1840
- if (!Object.hasOwn(fields, key))
1841
- return false;
1842
- const type = fields[key].type;
1843
- if (type === 'multi_select')
1844
- return typeof value !== 'string' && (!Array.isArray(value) || value.some(item => typeof item !== 'string'));
1845
- return typeof value === 'object' && type !== 'contact_reference';
1846
- });
1847
- const { anomalies } = normalizeContactProperties(values, { fields });
1848
- if (invalidShape || anomalies.length) {
1849
- const key = invalidShape?.[0] ?? anomalies[0].key;
1850
- return contactInputError('validation_failed', `custom_properties.${key} does not match its registered ${fields[key].type} definition or options`);
1851
- }
1852
- propertyWarnings = unknownCustomPropertyWarnings(custom_properties, fields);
1823
+ const validation = validateContactPropertyPatch(custom_properties, fields);
1824
+ if (validation.error)
1825
+ return contactInputError('validation_failed', validation.error);
1826
+ propertyWarnings = validation.warnings;
1853
1827
  }
1854
1828
  const embedOk = await hasCompaniesEmbed(projectId);
1855
1829
  const updates = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.151",
3
+ "version": "1.0.153",
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",