@sendinel/mcp-server 1.0.96 → 1.0.98
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/build/tools/campaigns.js +31 -10
- package/package.json +1 -1
package/build/tools/campaigns.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { CAMPAIGN_BRANCH_CONDITIONS, campaignBranchConditionValue } from "@sendinel/domain";
|
|
1
|
+
import { CAMPAIGN_BRANCH_CONDITIONS, campaignBranchConditionValue, isContactPropertyConditionList, STEP_PROPERTY_CONDITIONS_ERROR, validateStepPropertyRules } from "@sendinel/domain";
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import Anthropic from '@anthropic-ai/sdk';
|
|
4
|
-
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
|
+
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, } from '@sendinel/domain';
|
|
5
5
|
// Subpath import, not the barrel above -- reply-actions.ts uses node:crypto,
|
|
6
6
|
// and MCP-server-side that's fine (this never runs in an Edge Function), but
|
|
7
7
|
// keeping it off the shared barrel avoids re-creating the Edge-Function
|
|
@@ -764,6 +764,10 @@ export function registerCampaignTools(server) {
|
|
|
764
764
|
return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
|
|
765
765
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
766
766
|
});
|
|
767
|
+
const propertyConditions = z.array(z.union([
|
|
768
|
+
z.object({ key: z.string(), op: z.enum(['eq', 'neq']), value: z.string() }).strict(),
|
|
769
|
+
z.object({ key: z.string(), op: z.literal('exists') }).strict(),
|
|
770
|
+
])).min(1).max(10).refine(isContactPropertyConditionList, STEP_PROPERTY_CONDITIONS_ERROR).nullable().optional();
|
|
767
771
|
server.registerTool('add_campaign_step', {
|
|
768
772
|
title: 'Add Campaign Step',
|
|
769
773
|
annotations: {
|
|
@@ -785,10 +789,15 @@ export function registerCampaignTools(server) {
|
|
|
785
789
|
variant_label: z.string().optional().default('A').describe('Variant label for A/B testing (default: A). Use B, C, etc. for additional variants.'),
|
|
786
790
|
survey_id: z.string().uuid().optional(),
|
|
787
791
|
brand_asset_ids: z.array(z.string().uuid()).optional().describe('Stable brand_media_assets ids explicitly referenced by this step.'),
|
|
792
|
+
skip_if_property: propertyConditions.describe('Skip when ALL 1–10 conditions match current custom_properties. Missing/null keys never match. null clears.'),
|
|
793
|
+
send_only_if_property: propertyConditions.describe('Send only when ALL 1–10 conditions match current custom_properties. Unknown/non-match skips. null clears.'),
|
|
788
794
|
skip_if_event: z.string().trim().min(1).optional().describe('Skip this step at send time if the contact already has an event with this name; the journey continues.'),
|
|
789
795
|
send_only_if_event: z.string().trim().min(1).optional().describe('Send this step only if the contact has an event with this name by send time; otherwise skip it and continue the journey.'),
|
|
790
796
|
},
|
|
791
|
-
}, async ({ campaign_id, step_order, subject, body_html, body_text, blocks, delay_days, delay_hours, variant_label, survey_id, brand_asset_ids, skip_if_event, send_only_if_event }) => {
|
|
797
|
+
}, async ({ campaign_id, step_order, subject, body_html, body_text, blocks, delay_days, delay_hours, variant_label, survey_id, brand_asset_ids, skip_if_event, send_only_if_event, skip_if_property, send_only_if_property }) => {
|
|
798
|
+
const properties = validateStepPropertyRules({ skip_if_property, send_only_if_property });
|
|
799
|
+
if (!properties.ok)
|
|
800
|
+
return j({ error: properties.error, code: "INVALID_INPUT" });
|
|
792
801
|
const renderVars = {};
|
|
793
802
|
if (survey_id) {
|
|
794
803
|
const surveyLink = await resolveSurveyShortLink(db(), getProjectId(), survey_id);
|
|
@@ -803,7 +812,7 @@ export function registerCampaignTools(server) {
|
|
|
803
812
|
const renderedText = body_text !== undefined && survey_id ? renderTemplateVars(body_text, renderVars) : body_text;
|
|
804
813
|
const { data, error } = await db()
|
|
805
814
|
.from('campaign_steps')
|
|
806
|
-
.insert({ campaign_id, step_order, subject: renderedSubject, body_html: renderedHtml, body_text: renderedText ?? (normalizedBlocks?.length ? blocksToText(normalizedBlocks) : undefined), blocks: normalizedBlocks ?? null, delay_days, delay_hours, variant_label: variant_label ?? 'A', skip_if_event: skip_if_event ?? null, send_only_if_event: send_only_if_event ?? null })
|
|
815
|
+
.insert({ campaign_id, step_order, subject: renderedSubject, body_html: renderedHtml, body_text: renderedText ?? (normalizedBlocks?.length ? blocksToText(normalizedBlocks) : undefined), blocks: normalizedBlocks ?? null, delay_days, delay_hours, variant_label: variant_label ?? 'A', skip_if_event: skip_if_event ?? null, send_only_if_event: send_only_if_event ?? null, ...properties.rules })
|
|
807
816
|
.select()
|
|
808
817
|
.single();
|
|
809
818
|
if (error)
|
|
@@ -818,6 +827,7 @@ export function registerCampaignTools(server) {
|
|
|
818
827
|
}).catch((err) => {
|
|
819
828
|
console.warn('Failed to record brand asset usage for MCP campaign step:', err);
|
|
820
829
|
});
|
|
830
|
+
await logAudit({ action: 'campaign.step_created', resourceType: 'campaign_step', resourceId: data.id, details: { property_rules: properties.rules } });
|
|
821
831
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
822
832
|
});
|
|
823
833
|
server.registerTool('update_campaign_status', {
|
|
@@ -2321,7 +2331,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
|
|
|
2321
2331
|
.limit(1)
|
|
2322
2332
|
.maybeSingle();
|
|
2323
2333
|
if (suppression)
|
|
2324
|
-
return txt(
|
|
2334
|
+
return { ...txt('Error: Recipient is suppressed'), code: 'invalid_recipient' };
|
|
2325
2335
|
// Idempotency check
|
|
2326
2336
|
if (idempotency_key) {
|
|
2327
2337
|
const { data: existing } = await db()
|
|
@@ -2359,8 +2369,10 @@ Do not include body_html in your response; Sendinel renders body_html from block
|
|
|
2359
2369
|
.eq('id', template_id)
|
|
2360
2370
|
.eq('project_id', projectId)
|
|
2361
2371
|
.maybeSingle();
|
|
2362
|
-
if (templateError)
|
|
2363
|
-
|
|
2372
|
+
if (templateError) {
|
|
2373
|
+
console.error('MCP send template lookup failed:', templateError);
|
|
2374
|
+
return { ...txt('Error: Internal server error'), ...sendError('internal_error') };
|
|
2375
|
+
}
|
|
2364
2376
|
if (!template)
|
|
2365
2377
|
return txt('Error: Template not found');
|
|
2366
2378
|
const { data: project } = await adminDb().from('projects').select('brand_settings').eq('id', projectId).maybeSingle();
|
|
@@ -2415,10 +2427,13 @@ Do not include body_html in your response; Sendinel renders body_html from block
|
|
|
2415
2427
|
assertSendAccepted(sendResult);
|
|
2416
2428
|
}
|
|
2417
2429
|
catch (error) {
|
|
2430
|
+
console.error('MCP transactional send failed:', error);
|
|
2418
2431
|
if (error instanceof SendRefusedError) {
|
|
2419
2432
|
await db().from('email_log').insert({ project_id: projectId, site_id, to_email: to, from_email: site.from_email, subject: resolvedSubject, category: 'transactional', status: 'failed', error_message: error.message });
|
|
2420
2433
|
}
|
|
2421
|
-
|
|
2434
|
+
const safe = providerSendError(error);
|
|
2435
|
+
const code = error instanceof SendRefusedError ? error.refusal.code : safe.code;
|
|
2436
|
+
return { ...txt(`Error: Send failed — ${safe.error} (${code})`), error: safe.error, code };
|
|
2422
2437
|
}
|
|
2423
2438
|
// Log to email_log
|
|
2424
2439
|
const { data: logRow } = await db().from('email_log').insert({
|
|
@@ -3101,7 +3116,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3101
3116
|
return j({ error: campaignErr?.message ?? 'Campaign not found', code: 'NOT_FOUND' });
|
|
3102
3117
|
const { data, error } = await db()
|
|
3103
3118
|
.from('campaign_steps')
|
|
3104
|
-
.select('id, step_order, subject, body_html, body_text, blocks, delay_days, delay_hours, skip_if_event, send_only_if_event, created_at, updated_at')
|
|
3119
|
+
.select('id, step_order, subject, body_html, body_text, blocks, delay_days, delay_hours, skip_if_event, send_only_if_event, skip_if_property, send_only_if_property, created_at, updated_at')
|
|
3105
3120
|
.eq('campaign_id', campaign_id)
|
|
3106
3121
|
.order('step_order', { ascending: true });
|
|
3107
3122
|
if (error)
|
|
@@ -3127,6 +3142,8 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3127
3142
|
delay_hours: z.number().int().min(0).optional(),
|
|
3128
3143
|
step_order: z.number().int().min(0).optional(),
|
|
3129
3144
|
survey_id: z.string().uuid().optional(),
|
|
3145
|
+
skip_if_property: propertyConditions.describe('Skip when ALL 1–10 conditions match current custom_properties. Missing/null keys never match. null clears.'),
|
|
3146
|
+
send_only_if_property: propertyConditions.describe('Send only when ALL 1–10 conditions match current custom_properties. Unknown/non-match skips. null clears.'),
|
|
3130
3147
|
skip_if_event: z.string().trim().min(1).nullable().optional().describe('Event name that skips this step at send time; null clears the rule.'),
|
|
3131
3148
|
send_only_if_event: z.string().trim().min(1).nullable().optional().describe('Event required for this step to send; null clears the rule.'),
|
|
3132
3149
|
link_actions: z.array(z.discriminatedUnion('action', [
|
|
@@ -3143,6 +3160,9 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3143
3160
|
}, async ({ step_id, ...updates }) => {
|
|
3144
3161
|
const projectId = getProjectId();
|
|
3145
3162
|
const { survey_id, ...stepUpdates } = updates;
|
|
3163
|
+
const properties = validateStepPropertyRules(stepUpdates);
|
|
3164
|
+
if (!properties.ok)
|
|
3165
|
+
return j({ error: properties.error, code: "INVALID_INPUT" });
|
|
3146
3166
|
const { data: step, error: stepErr } = await db()
|
|
3147
3167
|
.from('campaign_steps')
|
|
3148
3168
|
.select('id, campaign_id')
|
|
@@ -3179,10 +3199,11 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3179
3199
|
.from('campaign_steps')
|
|
3180
3200
|
.update({ ...renderedUpdates, updated_at: new Date().toISOString() })
|
|
3181
3201
|
.eq('id', step_id)
|
|
3182
|
-
.select('id, step_order, subject, delay_days, delay_hours, skip_if_event, send_only_if_event, updated_at')
|
|
3202
|
+
.select('id, step_order, subject, delay_days, delay_hours, skip_if_event, send_only_if_event, skip_if_property, send_only_if_property, updated_at')
|
|
3183
3203
|
.single();
|
|
3184
3204
|
if (error)
|
|
3185
3205
|
return j({ error: error.message });
|
|
3206
|
+
await logAudit({ action: 'campaign.step_updated', resourceType: 'campaign_step', resourceId: data.id, details: { property_rules: properties.rules } });
|
|
3186
3207
|
return j({ step: data, ok: true });
|
|
3187
3208
|
});
|
|
3188
3209
|
server.registerTool('delete_campaign_step', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.98",
|
|
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",
|