@sendinel/mcp-server 1.0.212 → 1.0.213
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/lib/how-to-recipes.js +4 -4
- package/build/tools/campaigns.js +175 -105
- package/build/tools/templates.js +2 -2
- package/package.json +1 -1
|
@@ -112,12 +112,12 @@ export const HOW_TO_RECIPES = [
|
|
|
112
112
|
requires: { scopes: ['write'], tester_ok: false },
|
|
113
113
|
steps: [sender,
|
|
114
114
|
{ n: 2, tool: 'create_campaign', args_example: { site_id: from(1, '0.id'), name: 'Welcome new signups', type: 'triggered', trigger_event_name: 'signup', send_class: 'product' }, note: 'Confirm the actual event emitted by the integration; signup is an example. This creates a draft.' },
|
|
115
|
-
{ n: 3, tool: 'add_campaign_step', args_example: { campaign_id: from(2, 'id'), step_order: 0, subject: 'Welcome!', body_html: '<p>Welcome aboard.</p>', delay_days: 0 } },
|
|
115
|
+
{ n: 3, tool: 'add_campaign_step', args_example: { campaign_id: from(2, 'id'), step_order: 0, subject: 'Welcome!', body_html: '<p>Welcome aboard.</p>', delay_days: 0 }, note: 'Optional: pass template_id (+ template_vars) from create_template/list_templates instead of body_html.' },
|
|
116
116
|
{ n: 4, tool: 'send_test_email', args_example: { to_email: 'person@example.com', site_id: from(1, '0.id'), step_id: from(3, 'id'), dry_run: true }, note: 'Use an authorized preview address. Keep dry_run:true: render only, without approval.' },
|
|
117
117
|
{ n: 5, tool: 'activate_campaigns', args_example: { campaign_ids: [from(2, 'id')] }, approval, note: 'Show the campaign content and approve_url to the person; stop until approved.' },
|
|
118
118
|
{ n: 6, tool: 'activate_campaigns', args_example: { approval_id: from(5, 'approval_id') }, approval, note: 'Only after approval. Activation turns on the saved campaign; launch_campaign instead generates a NEW campaign from briefs.' },
|
|
119
119
|
],
|
|
120
|
-
gotchas: [projectRule, previewRule, 'Activation does not emit signup events or enroll historical signups. Verify the integration fires the configured event.'],
|
|
120
|
+
gotchas: [projectRule, previewRule, 'Activation does not emit signup events or enroll historical signups. Verify the integration fires the configured event.', 'A template_id on add_campaign_step is rendered once at step creation, so later template edits do not change existing steps.'],
|
|
121
121
|
done_when: 'Activation succeeds and the integration’s signup event is configured; end-to-end delivery remains unverified until an authorized signup test completes.',
|
|
122
122
|
},
|
|
123
123
|
{
|
|
@@ -155,9 +155,9 @@ export const HOW_TO_RECIPES = [
|
|
|
155
155
|
{ n: 4, tool: 'preview_email', args_example: { site_id: from(1, '0.id'), subject: from(3, 'template.subject_hint'), body_html: from(3, 'template.body_html') }, note: 'Review the rendered content with sample contact data; this sends nothing.' },
|
|
156
156
|
{ n: 5, tool: 'list_templates', args_example: { search: 'Product update' }, note: 'Verify the reusable template is discoverable; paginate if truncated.' },
|
|
157
157
|
{ n: 6, tool: 'create_campaign', args_example: { site_id: from(1, '0.id'), name: 'Product update sequence', type: 'sequence' }, note: 'Optional use in a campaign: creates a new draft; use an existing draft instead if appropriate.' },
|
|
158
|
-
{ n: 7, tool: 'add_campaign_step', args_example: { campaign_id: from(6, 'id'), step_order: 0, subject: from(3, 'template.subject_hint'), body_html: from(3, 'template.body_html') }, note: '
|
|
158
|
+
{ n: 7, tool: 'add_campaign_step', args_example: { campaign_id: from(6, 'id'), step_order: 0, subject: from(3, 'template.subject_hint'), body_html: from(3, 'template.body_html') }, note: 'Optional: pass template_id (+ template_vars) from create_template/list_templates instead of body_html. This draft is not activated.' },
|
|
159
159
|
],
|
|
160
|
-
gotchas: [projectRule, previewRule, '
|
|
160
|
+
gotchas: [projectRule, previewRule, 'A template_id on add_campaign_step is rendered once at step creation, so later template edits do not change existing steps. Sequences need a separate enrollment/activation plan and approval. For a one-off segment broadcast use send_broadcast with copied html and subject, segment_id and optional send_at; approval is required.'],
|
|
161
161
|
done_when: 'The template is saved, previewed and listed; if requested, its content is copied into an unactivated campaign step.',
|
|
162
162
|
},
|
|
163
163
|
{
|
package/build/tools/campaigns.js
CHANGED
|
@@ -67,6 +67,54 @@ function stripHtml(value) {
|
|
|
67
67
|
.replace(/\s+/g, ' ')
|
|
68
68
|
.trim();
|
|
69
69
|
}
|
|
70
|
+
// One render path for send_transactional, send_awaiting_reply, and add_campaign_step.
|
|
71
|
+
async function renderStoredCampaignTemplate(args) {
|
|
72
|
+
const { data: template, error: templateError } = await db()
|
|
73
|
+
.from('campaign_templates')
|
|
74
|
+
.select('id, subject_hint, body_html, body_text, format')
|
|
75
|
+
.eq('id', args.templateId)
|
|
76
|
+
.eq('project_id', args.projectId)
|
|
77
|
+
.maybeSingle();
|
|
78
|
+
if (templateError)
|
|
79
|
+
return { ok: false, reason: 'lookup_failed', dbError: templateError };
|
|
80
|
+
if (!template)
|
|
81
|
+
return { ok: false, reason: 'not_found' };
|
|
82
|
+
const { data: project } = await adminDb().from('projects').select('brand_settings').eq('id', args.projectId).maybeSingle();
|
|
83
|
+
const projectBrand = (project?.brand_settings ?? null);
|
|
84
|
+
const siteBrand = (args.site.brand_voice ?? null);
|
|
85
|
+
const mergedBrand = projectBrand || siteBrand ? { ...(projectBrand ?? {}), ...(siteBrand ?? {}) } : null;
|
|
86
|
+
const brandVars = resolveBrandTokens(mergedBrand, args.site.name || undefined, { address: args.site.physical_address ?? undefined });
|
|
87
|
+
const brandWarningCodes = brandWarnings(mergedBrand);
|
|
88
|
+
const vars = args.templateVars ?? {};
|
|
89
|
+
const renderVars = { ...brandVars, ...vars };
|
|
90
|
+
const subjectFallingBack = !args.subject?.trim();
|
|
91
|
+
const requiredVars = deriveTemplateVariables({
|
|
92
|
+
body_html: template.format === 'text' ? null : (template.body_html ?? ''),
|
|
93
|
+
body_text: template.format === 'text' ? (template.body_text ?? '') : null,
|
|
94
|
+
subject_hint: subjectFallingBack ? (template.subject_hint ?? '') : null,
|
|
95
|
+
});
|
|
96
|
+
const satisfied = new Set([
|
|
97
|
+
...Object.keys(vars),
|
|
98
|
+
'brand_name', 'brand_logo_url', 'brand_bg_color', 'brand_text_color', 'brand_accent_color',
|
|
99
|
+
'brand_logo_html', 'brand_font', 'brand_address',
|
|
100
|
+
...Object.keys(NAME_FALLBACKS),
|
|
101
|
+
]);
|
|
102
|
+
const missing = requiredVars.filter((name) => !satisfied.has(name));
|
|
103
|
+
if (missing.length)
|
|
104
|
+
return { ok: false, reason: 'missing_vars', missing, brandWarningCodes };
|
|
105
|
+
let html;
|
|
106
|
+
let text;
|
|
107
|
+
if (template.format === 'text') {
|
|
108
|
+
text = renderTemplateVars(template.body_text ?? '', renderVars, { brandTokens: brandVars });
|
|
109
|
+
html = undefined;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
html = renderTemplateVars(template.body_html ?? '', renderVars, { brandTokens: brandVars });
|
|
113
|
+
text = args.callerText ?? stripHtml(html);
|
|
114
|
+
}
|
|
115
|
+
const subject = args.subject?.trim() || renderTemplateVars(template.subject_hint ?? '', renderVars, { brandTokens: brandVars });
|
|
116
|
+
return { ok: true, subject, html, text, brandWarningCodes };
|
|
117
|
+
}
|
|
70
118
|
function normalizeTriggerValue(value) {
|
|
71
119
|
if (typeof value !== 'string')
|
|
72
120
|
return null;
|
|
@@ -974,14 +1022,16 @@ export function registerCampaignTools(server) {
|
|
|
974
1022
|
idempotentHint: false,
|
|
975
1023
|
openWorldHint: false,
|
|
976
1024
|
},
|
|
977
|
-
description: 'Add an email step to a campaign. Requires a non-blank subject and body_html, body_text, or blocks that render to content
|
|
1025
|
+
description: 'Add an email step to a campaign. Requires a non-blank subject and body_html, body_text, or blocks that render to content, unless template_id is set. Optionally render from a template via template_id. When template_id is set without body content, the template is rendered once into subject and body_html. For A/B testing, add multiple steps with the same step_order but different variant_label values.',
|
|
978
1026
|
inputSchema: z.object({
|
|
979
1027
|
campaign_id: z.string().uuid(),
|
|
980
1028
|
step_order: z.number().int().min(0).describe('Position in sequence (0 = first)'),
|
|
981
|
-
subject: z.string().min(1),
|
|
1029
|
+
subject: z.string().min(1).optional().describe('Email subject. Required unless template_id supplies a subject_hint.'),
|
|
982
1030
|
body_html: z.string().optional(),
|
|
983
1031
|
body_text: z.string().optional(),
|
|
984
1032
|
blocks: z.array(z.record(z.string(), z.unknown())).optional().describe('Optional structured Composer blocks that produced body_html.'),
|
|
1033
|
+
template_id: z.string().uuid().optional().describe('campaign_templates row to render into this step. When set, body_html/blocks are not required; explicit body_html/body_text/blocks win over the template body.'),
|
|
1034
|
+
template_vars: z.record(z.string(), z.string()).optional().describe('Variable values for template_id rendering. Wins over brand tokens on key collision.'),
|
|
985
1035
|
delay_days: z.number().int().min(0).optional().default(0),
|
|
986
1036
|
delay_hours: z.number().int().min(0).max(23).optional().default(0),
|
|
987
1037
|
variant_label: z.string().optional().default('A').describe('Variant label for A/B testing (default: A). Use B, C, etc. for additional variants.'),
|
|
@@ -994,7 +1044,7 @@ export function registerCampaignTools(server) {
|
|
|
994
1044
|
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.'),
|
|
995
1045
|
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.'),
|
|
996
1046
|
}).refine(absencePairValid, { message: absencePairError }),
|
|
997
|
-
}, 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_events, send_only_if_events, send_only_if_events_mode, send_only_if_no_event, send_only_if_no_event_days, skip_if_property, send_only_if_property }) => {
|
|
1047
|
+
}, async ({ campaign_id, step_order, subject, body_html, body_text, blocks, template_id, template_vars, delay_days, delay_hours, variant_label, survey_id, brand_asset_ids, skip_if_event, send_only_if_event, skip_if_events, send_only_if_events, send_only_if_events_mode, send_only_if_no_event, send_only_if_no_event_days, skip_if_property, send_only_if_property }) => {
|
|
998
1048
|
try {
|
|
999
1049
|
const absence = validateStepAbsenceRule({ send_only_if_no_event, send_only_if_no_event_days });
|
|
1000
1050
|
if (!absence.ok)
|
|
@@ -1005,22 +1055,29 @@ export function registerCampaignTools(server) {
|
|
|
1005
1055
|
const properties = validateStepPropertyRules({ skip_if_property, send_only_if_property });
|
|
1006
1056
|
if (!properties.ok)
|
|
1007
1057
|
return j({ error: properties.error, code: "INVALID_INPUT" });
|
|
1058
|
+
if (template_id && body_html !== undefined) {
|
|
1059
|
+
return j({ error: "Provide either 'template_id' or 'body_html', not both", code: 'INVALID_INPUT' });
|
|
1060
|
+
}
|
|
1008
1061
|
const normalizedBlocks = blocks ? normalizeComposerBlocks(blocks) : undefined;
|
|
1009
1062
|
const htmlFromBlocks = normalizedBlocks?.length ? blocksToHtml(normalizedBlocks) : undefined;
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
const hasBody = body_html?.trim() || body_text?.trim()
|
|
1063
|
+
const renderingTemplate = Boolean(template_id) && body_html === undefined && body_text === undefined && blocks === undefined;
|
|
1064
|
+
const explicitHasBody = () => Boolean(body_html?.trim() || body_text?.trim()
|
|
1013
1065
|
|| htmlFromBlocks?.replace(/<[^>]*>/g, '').trim()
|
|
1014
|
-
|| normalizedBlocks?.some((block) => block.type === 'image' && block.url);
|
|
1015
|
-
if (!
|
|
1016
|
-
|
|
1066
|
+
|| normalizedBlocks?.some((block) => block.type === 'image' && block.url));
|
|
1067
|
+
if (!renderingTemplate) {
|
|
1068
|
+
if (!subject?.trim())
|
|
1069
|
+
return j({ error: 'A step needs a non-blank subject.', code: 'validation_failed' });
|
|
1070
|
+
if (!explicitHasBody()) {
|
|
1071
|
+
return j({ error: 'A step needs body_html or body_text (or blocks that render to content).', code: 'validation_failed' });
|
|
1072
|
+
}
|
|
1017
1073
|
}
|
|
1074
|
+
const projectId = getProjectId();
|
|
1018
1075
|
const { data: campaign, error: campaignError } = await db()
|
|
1019
1076
|
.schema('email')
|
|
1020
1077
|
.from('campaigns')
|
|
1021
|
-
.select('id')
|
|
1078
|
+
.select('id, site_id')
|
|
1022
1079
|
.eq('id', campaign_id)
|
|
1023
|
-
.eq('project_id',
|
|
1080
|
+
.eq('project_id', projectId)
|
|
1024
1081
|
.maybeSingle();
|
|
1025
1082
|
if (campaignError)
|
|
1026
1083
|
return dbErrorResult(campaignError);
|
|
@@ -1028,24 +1085,81 @@ export function registerCampaignTools(server) {
|
|
|
1028
1085
|
return { ...j({ error: 'Campaign not found.', code: 'not_found' }), isError: true };
|
|
1029
1086
|
const renderVars = {};
|
|
1030
1087
|
if (survey_id) {
|
|
1031
|
-
const surveyLink = await resolveSurveyShortLink(db(),
|
|
1088
|
+
const surveyLink = await resolveSurveyShortLink(db(), projectId, survey_id);
|
|
1032
1089
|
if (!surveyLink)
|
|
1033
1090
|
return j({ error: 'Survey has no short link yet', code: 'SURVEY_LINK_NOT_READY' });
|
|
1034
1091
|
renderVars.survey_link = surveyLink;
|
|
1035
1092
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1093
|
+
let bakedSubject = subject;
|
|
1094
|
+
let bakedHtml = body_html;
|
|
1095
|
+
let bakedText = body_text;
|
|
1096
|
+
if (renderingTemplate && template_id) {
|
|
1097
|
+
let siteForTemplate = {};
|
|
1098
|
+
if (campaign.site_id) {
|
|
1099
|
+
const { data: site, error: siteError } = await db()
|
|
1100
|
+
.from('sites')
|
|
1101
|
+
.select('name, brand_voice, physical_address')
|
|
1102
|
+
.eq('id', campaign.site_id)
|
|
1103
|
+
.eq('project_id', projectId)
|
|
1104
|
+
.maybeSingle();
|
|
1105
|
+
if (siteError)
|
|
1106
|
+
return dbErrorResult(siteError);
|
|
1107
|
+
if (site)
|
|
1108
|
+
siteForTemplate = site;
|
|
1109
|
+
}
|
|
1110
|
+
const rendered = await renderStoredCampaignTemplate({
|
|
1111
|
+
templateId: template_id,
|
|
1112
|
+
templateVars: { ...(template_vars ?? {}), ...renderVars },
|
|
1113
|
+
subject,
|
|
1114
|
+
projectId,
|
|
1115
|
+
site: siteForTemplate,
|
|
1116
|
+
});
|
|
1117
|
+
if (!rendered.ok) {
|
|
1118
|
+
if (rendered.reason === 'lookup_failed')
|
|
1119
|
+
return dbErrorResult(rendered.dbError);
|
|
1120
|
+
if (rendered.reason === 'not_found')
|
|
1121
|
+
return { ...j({ error: 'Template not found.', code: 'not_found' }), isError: true };
|
|
1122
|
+
return j({ error: `Missing required template variable(s): ${rendered.missing.join(', ')}`, code: 'validation_failed' });
|
|
1123
|
+
}
|
|
1124
|
+
bakedSubject = rendered.subject;
|
|
1125
|
+
bakedHtml = rendered.html === undefined ? undefined : stripActiveEmailContent(rendered.html).html;
|
|
1126
|
+
bakedText = rendered.text;
|
|
1127
|
+
if (!bakedSubject?.trim())
|
|
1128
|
+
return j({ error: 'A step needs a non-blank subject.', code: 'validation_failed' });
|
|
1129
|
+
const bakedHasBody = bakedHtml?.trim() || bakedText?.trim();
|
|
1130
|
+
if (!bakedHasBody) {
|
|
1131
|
+
return j({ error: 'A step needs body_html or body_text (or blocks that render to content).', code: 'validation_failed' });
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
else if (template_id) {
|
|
1135
|
+
const { data: template, error: templateError } = await db()
|
|
1136
|
+
.from('campaign_templates')
|
|
1137
|
+
.select('id')
|
|
1138
|
+
.eq('id', template_id)
|
|
1139
|
+
.eq('project_id', projectId)
|
|
1140
|
+
.maybeSingle();
|
|
1141
|
+
if (templateError)
|
|
1142
|
+
return dbErrorResult(templateError);
|
|
1143
|
+
if (!template)
|
|
1144
|
+
return { ...j({ error: 'Template not found.', code: 'not_found' }), isError: true };
|
|
1145
|
+
}
|
|
1146
|
+
const renderedSubject = survey_id && bakedSubject !== undefined ? renderTemplateVars(bakedSubject, renderVars) : bakedSubject;
|
|
1147
|
+
const renderedHtml = renderingTemplate
|
|
1148
|
+
? (survey_id && bakedHtml !== undefined ? renderTemplateVars(bakedHtml, renderVars) : bakedHtml)
|
|
1149
|
+
: (body_html !== undefined && survey_id ? renderTemplateVars(body_html, renderVars) : body_html ?? htmlFromBlocks);
|
|
1150
|
+
const renderedText = renderingTemplate
|
|
1151
|
+
? (survey_id && bakedText !== undefined ? renderTemplateVars(bakedText, renderVars) : bakedText)
|
|
1152
|
+
: (body_text !== undefined && survey_id ? renderTemplateVars(body_text, renderVars) : body_text);
|
|
1039
1153
|
const { data, error } = await withCreatedItems('campaign_steps', db()
|
|
1040
1154
|
.from('campaign_steps')
|
|
1041
|
-
.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, ...events.rules, ...absence.rules, ...properties.rules })
|
|
1155
|
+
.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', template_id: template_id ?? null, template_vars: template_vars ?? null, skip_if_event: skip_if_event ?? null, send_only_if_event: send_only_if_event ?? null, ...events.rules, ...absence.rules, ...properties.rules })
|
|
1042
1156
|
.select()
|
|
1043
1157
|
.single());
|
|
1044
1158
|
if (error)
|
|
1045
1159
|
return dbErrorResult(error);
|
|
1046
1160
|
await recordBrandAssetUsage({
|
|
1047
1161
|
client: db(),
|
|
1048
|
-
projectId
|
|
1162
|
+
projectId,
|
|
1049
1163
|
source: 'campaign_step',
|
|
1050
1164
|
resourceId: data.id,
|
|
1051
1165
|
html: renderedHtml,
|
|
@@ -1053,7 +1167,7 @@ export function registerCampaignTools(server) {
|
|
|
1053
1167
|
}).catch((err) => {
|
|
1054
1168
|
console.warn('Failed to record brand asset usage for MCP campaign step:', err);
|
|
1055
1169
|
});
|
|
1056
|
-
await logAudit({ action: 'campaign.step_created', resourceType: 'campaign_step', resourceId: data.id, details: { property_rules: properties.rules } });
|
|
1170
|
+
await logAudit({ action: 'campaign.step_created', resourceType: 'campaign_step', resourceId: data.id, details: { property_rules: properties.rules, template_id: template_id ?? null } });
|
|
1057
1171
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
1058
1172
|
}
|
|
1059
1173
|
catch (error) {
|
|
@@ -2708,53 +2822,31 @@ Do not include body_html in your response; Sendinel renders body_html from block
|
|
|
2708
2822
|
let resolvedText = text;
|
|
2709
2823
|
let brandWarningCodes = [];
|
|
2710
2824
|
if (template_id) {
|
|
2711
|
-
const
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2825
|
+
const rendered = await renderStoredCampaignTemplate({
|
|
2826
|
+
templateId: template_id,
|
|
2827
|
+
templateVars: template_vars,
|
|
2828
|
+
subject,
|
|
2829
|
+
projectId,
|
|
2830
|
+
site,
|
|
2831
|
+
callerText: text,
|
|
2832
|
+
});
|
|
2833
|
+
if (!rendered.ok) {
|
|
2834
|
+
if (rendered.reason === 'lookup_failed') {
|
|
2835
|
+
console.error('MCP send template lookup failed:', rendered.dbError);
|
|
2836
|
+
return { ...txt('Error: Internal server error'), ...sendError('internal_error') };
|
|
2837
|
+
}
|
|
2838
|
+
if (rendered.reason === 'not_found')
|
|
2839
|
+
return txt('Error: Template not found');
|
|
2840
|
+
if (rendered.brandWarningCodes.length)
|
|
2841
|
+
console.warn('MCP send brand warnings', { projectId, site_id, warnings: rendered.brandWarningCodes });
|
|
2842
|
+
return txt(`Error: Missing required template variable(s): ${rendered.missing.join(', ')}`);
|
|
2720
2843
|
}
|
|
2721
|
-
|
|
2722
|
-
return txt('Error: Template not found');
|
|
2723
|
-
const { data: project } = await adminDb().from('projects').select('brand_settings').eq('id', projectId).maybeSingle();
|
|
2724
|
-
const projectBrand = (project?.brand_settings ?? null);
|
|
2725
|
-
const siteBrand = (site.brand_voice ?? null);
|
|
2726
|
-
const mergedBrand = projectBrand || siteBrand ? { ...(projectBrand ?? {}), ...(siteBrand ?? {}) } : null;
|
|
2727
|
-
const brandVars = resolveBrandTokens(mergedBrand, site.name || undefined, { address: site.physical_address ?? undefined });
|
|
2728
|
-
brandWarningCodes = brandWarnings(mergedBrand);
|
|
2844
|
+
brandWarningCodes = rendered.brandWarningCodes;
|
|
2729
2845
|
if (brandWarningCodes.length)
|
|
2730
2846
|
console.warn('MCP send brand warnings', { projectId, site_id, warnings: brandWarningCodes });
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
const requiredVars = deriveTemplateVariables({
|
|
2735
|
-
body_html: template.format === 'text' ? null : (template.body_html ?? ''),
|
|
2736
|
-
body_text: template.format === 'text' ? (template.body_text ?? '') : null,
|
|
2737
|
-
subject_hint: subjectFallingBack ? (template.subject_hint ?? '') : null,
|
|
2738
|
-
});
|
|
2739
|
-
const satisfied = new Set([
|
|
2740
|
-
...Object.keys(vars),
|
|
2741
|
-
'brand_name', 'brand_logo_url', 'brand_bg_color', 'brand_text_color', 'brand_accent_color',
|
|
2742
|
-
'brand_logo_html', 'brand_font', 'brand_address',
|
|
2743
|
-
...Object.keys(NAME_FALLBACKS),
|
|
2744
|
-
]);
|
|
2745
|
-
const missing = requiredVars.filter((name) => !satisfied.has(name));
|
|
2746
|
-
if (missing.length) {
|
|
2747
|
-
return txt(`Error: Missing required template variable(s): ${missing.join(', ')}`);
|
|
2748
|
-
}
|
|
2749
|
-
if (template.format === 'text') {
|
|
2750
|
-
resolvedText = renderTemplateVars(template.body_text ?? '', renderVars, { brandTokens: brandVars });
|
|
2751
|
-
resolvedHtml = undefined;
|
|
2752
|
-
}
|
|
2753
|
-
else {
|
|
2754
|
-
resolvedHtml = renderTemplateVars(template.body_html ?? '', renderVars, { brandTokens: brandVars });
|
|
2755
|
-
resolvedText = text ?? stripHtml(resolvedHtml);
|
|
2756
|
-
}
|
|
2757
|
-
resolvedSubject = subject?.trim() || renderTemplateVars(template.subject_hint ?? '', renderVars, { brandTokens: brandVars });
|
|
2847
|
+
resolvedSubject = rendered.subject;
|
|
2848
|
+
resolvedHtml = rendered.html;
|
|
2849
|
+
resolvedText = rendered.text;
|
|
2758
2850
|
}
|
|
2759
2851
|
if (resolvedHtml !== undefined)
|
|
2760
2852
|
resolvedHtml = stripActiveEmailContent(resolvedHtml).html;
|
|
@@ -2904,51 +2996,29 @@ Do not include body_html in your response; Sendinel renders body_html from block
|
|
|
2904
2996
|
let resolvedText = text;
|
|
2905
2997
|
let brandWarningCodes = [];
|
|
2906
2998
|
if (template_id) {
|
|
2907
|
-
const
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
return txt(`Error: ${dbToolError(templateError).error}`);
|
|
2915
|
-
if (!template)
|
|
2916
|
-
return txt('Error: Template not found');
|
|
2917
|
-
const { data: project } = await adminDb().from('projects').select('brand_settings').eq('id', projectId).maybeSingle();
|
|
2918
|
-
const projectBrand = (project?.brand_settings ?? null);
|
|
2919
|
-
const siteBrand = (site.brand_voice ?? null);
|
|
2920
|
-
const mergedBrand = projectBrand || siteBrand ? { ...(projectBrand ?? {}), ...(siteBrand ?? {}) } : null;
|
|
2921
|
-
const brandVars = resolveBrandTokens(mergedBrand, site.name || undefined, { address: site.physical_address ?? undefined });
|
|
2922
|
-
brandWarningCodes = brandWarnings(mergedBrand);
|
|
2923
|
-
if (brandWarningCodes.length)
|
|
2924
|
-
console.warn('MCP send brand warnings', { projectId, site_id, warnings: brandWarningCodes });
|
|
2925
|
-
const vars = template_vars ?? {};
|
|
2926
|
-
const renderVars = { ...brandVars, ...vars };
|
|
2927
|
-
const subjectFallingBack = !subject?.trim();
|
|
2928
|
-
const requiredVars = deriveTemplateVariables({
|
|
2929
|
-
body_html: template.format === 'text' ? null : (template.body_html ?? ''),
|
|
2930
|
-
body_text: template.format === 'text' ? (template.body_text ?? '') : null,
|
|
2931
|
-
subject_hint: subjectFallingBack ? (template.subject_hint ?? '') : null,
|
|
2999
|
+
const rendered = await renderStoredCampaignTemplate({
|
|
3000
|
+
templateId: template_id,
|
|
3001
|
+
templateVars: template_vars,
|
|
3002
|
+
subject,
|
|
3003
|
+
projectId,
|
|
3004
|
+
site,
|
|
3005
|
+
callerText: text,
|
|
2932
3006
|
});
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
return txt(`Error: Missing required template variable(s): ${missing.join(', ')}`);
|
|
3007
|
+
if (!rendered.ok) {
|
|
3008
|
+
if (rendered.reason === 'lookup_failed')
|
|
3009
|
+
return txt(`Error: ${dbToolError(rendered.dbError).error}`);
|
|
3010
|
+
if (rendered.reason === 'not_found')
|
|
3011
|
+
return txt('Error: Template not found');
|
|
3012
|
+
if (rendered.brandWarningCodes.length)
|
|
3013
|
+
console.warn('MCP send brand warnings', { projectId, site_id, warnings: rendered.brandWarningCodes });
|
|
3014
|
+
return txt(`Error: Missing required template variable(s): ${rendered.missing.join(', ')}`);
|
|
2942
3015
|
}
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
resolvedText = text ?? stripHtml(resolvedHtml);
|
|
2950
|
-
}
|
|
2951
|
-
resolvedSubject = subject?.trim() || renderTemplateVars(template.subject_hint ?? '', renderVars, { brandTokens: brandVars });
|
|
3016
|
+
brandWarningCodes = rendered.brandWarningCodes;
|
|
3017
|
+
if (brandWarningCodes.length)
|
|
3018
|
+
console.warn('MCP send brand warnings', { projectId, site_id, warnings: brandWarningCodes });
|
|
3019
|
+
resolvedSubject = rendered.subject;
|
|
3020
|
+
resolvedHtml = rendered.html;
|
|
3021
|
+
resolvedText = rendered.text;
|
|
2952
3022
|
}
|
|
2953
3023
|
if (resolvedHtml !== undefined)
|
|
2954
3024
|
resolvedHtml = stripActiveEmailContent(resolvedHtml).html;
|
package/build/tools/templates.js
CHANGED
|
@@ -241,10 +241,10 @@ export function registerTemplateTools(server) {
|
|
|
241
241
|
idempotentHint: false,
|
|
242
242
|
openWorldHint: false,
|
|
243
243
|
},
|
|
244
|
-
description: '
|
|
244
|
+
description: 'Create a template record from content you supply. `brief` is a stored note and does NOT trigger AI generation or use credits; use create_template_from_brief to AI-generate.',
|
|
245
245
|
inputSchema: {
|
|
246
246
|
name: templateName,
|
|
247
|
-
brief: z.string().min(1).describe('
|
|
247
|
+
brief: z.string().min(1).describe('Stored as brief_template only. Does NOT trigger AI generation or use credits. Use create_template_from_brief to AI-generate.'),
|
|
248
248
|
subject_hint: z.string().optional(),
|
|
249
249
|
body_html: z.string().optional(),
|
|
250
250
|
category: z.string().optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.213",
|
|
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",
|