@sendinel/mcp-server 1.0.138 → 1.0.140
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/README.md +2 -2
- package/build/lib/db-errors.d.ts +12 -0
- package/build/lib/db-errors.js +20 -0
- package/build/lib/url-validation.d.ts +3 -0
- package/build/lib/url-validation.js +15 -0
- package/build/tools/approvals.js +3 -2
- package/build/tools/campaigns.d.ts +2 -1
- package/build/tools/campaigns.js +122 -44
- package/build/tools/contacts.js +375 -267
- package/build/tools/content.js +9 -0
- package/build/tools/delivery-ops.js +6 -2
- package/build/tools/forms.js +30 -3
- package/build/tools/newsletters.js +41 -9
- package/build/tools/segments.js +57 -46
- package/build/tools/short-links.js +20 -14
- package/build/tools/surveys.js +41 -4
- package/build/tools/tool-search.js +1 -0
- package/build/tools/webhooks.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ preserves REST outcomes, and redacts failures. See the
|
|
|
18
18
|
[event-triggered campaign guide](../../docs/event-triggered-campaigns.md) for
|
|
19
19
|
inputs, idempotency, subscription requirements, and conditional step examples.
|
|
20
20
|
|
|
21
|
-
Sendinel currently exposes 35 tool groups and
|
|
21
|
+
Sendinel currently exposes 35 tool groups and 344 tools (gateway mode exposes 4 meta-tools, sharing how_to with full mode). These counts are checked against the tool registry in CI, so this table matches what ships.
|
|
22
22
|
|
|
23
23
|
| Group | Count | Purpose |
|
|
24
24
|
|-------|-------|---------|
|
|
@@ -34,7 +34,7 @@ Sendinel currently exposes 35 tool groups and 343 tools (gateway mode exposes 4
|
|
|
34
34
|
| `cloud-import` | 6 | Cloud storage import |
|
|
35
35
|
| `compound` | 4 | Higher-level campaign/template/site setup and delivery diagnosis workflows |
|
|
36
36
|
| `connector` | 6 | BYOD connector status, table discovery, schema/view setup, mapping, resync |
|
|
37
|
-
| `contacts` |
|
|
37
|
+
| `contacts` | 40 | Subscriber CRUD, saved views, tags, email log, suppression, merge, timeline |
|
|
38
38
|
| `content` | 14 | Content blocks, gated asset links, blog ingest and auto-publish, sources |
|
|
39
39
|
| `data` | 19 | Platform connectors, imports, API keys, migration jobs, notifications, tenant export |
|
|
40
40
|
| `data-proposals` | 3 | Data-derived campaign proposal generation, listing, approval |
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Caller-safe database errors. Never serialize database messages, details or hints. */
|
|
2
|
+
export declare function mapDbError(error: unknown): {
|
|
3
|
+
error: string;
|
|
4
|
+
code: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function dbErrorResult(error: unknown): {
|
|
7
|
+
isError: boolean;
|
|
8
|
+
content: {
|
|
9
|
+
type: 'text';
|
|
10
|
+
text: string;
|
|
11
|
+
}[];
|
|
12
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Caller-safe database errors. Never serialize database messages, details or hints. */
|
|
2
|
+
export function mapDbError(error) {
|
|
3
|
+
const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
|
|
4
|
+
if (code === '23505')
|
|
5
|
+
return { error: 'A record with these values already exists.', code: 'conflict' };
|
|
6
|
+
if (code === '23503' || code === 'PGRST116') {
|
|
7
|
+
return { error: 'The requested or referenced record was not found.', code: 'not_found' };
|
|
8
|
+
}
|
|
9
|
+
if (code.startsWith('23') || code.startsWith('22')) {
|
|
10
|
+
return { error: 'The supplied values are not valid. Check the input and try again.', code: 'validation_failed' };
|
|
11
|
+
}
|
|
12
|
+
return { error: 'The operation could not be completed. Please try again later.', code: 'internal_error' };
|
|
13
|
+
}
|
|
14
|
+
export function dbErrorResult(error) {
|
|
15
|
+
console.warn('[mcp] Database operation failed:', error);
|
|
16
|
+
return {
|
|
17
|
+
isError: true,
|
|
18
|
+
content: [{ type: 'text', text: JSON.stringify(mapDbError(error)) }],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** Redirect destinations may use HTTP or HTTPS, but never executable URL schemes. */
|
|
2
|
+
export declare function assertHttpUrl(raw: string): string;
|
|
3
|
+
export declare const RESERVED_LINK_SLUGS: Set<string>;
|
|
1
4
|
export declare function isSafeRedirectUrl(url: unknown): url is string;
|
|
2
5
|
export declare function isAllowedHttpsHostUrl(url: unknown, allowedDomains: readonly string[]): url is string;
|
|
3
6
|
export declare function normalizeActiveCampaignAccountUrl(accountUrl: string): string;
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/** Redirect destinations may use HTTP or HTTPS, but never executable URL schemes. */
|
|
2
|
+
export function assertHttpUrl(raw) {
|
|
3
|
+
const url = new URL(raw);
|
|
4
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
5
|
+
throw new Error('Only http and https URLs are supported.');
|
|
6
|
+
}
|
|
7
|
+
return url.toString();
|
|
8
|
+
}
|
|
9
|
+
// Shared /s namespace: system routes cannot be claimed by a tenant.
|
|
10
|
+
export const RESERVED_LINK_SLUGS = new Set([
|
|
11
|
+
'admin', 'api', 'app', 'login', 'dashboard', 's', 'a', 'www', 'support', 'billing',
|
|
12
|
+
'auth', 'static', 'assets', 'aup', 'ccpa', 'changelog', 'cloud-storage', 'demo-video',
|
|
13
|
+
'developer', 'embed', 'gdpr', 'getting-started', 'invite', 'legal', 'lp', 'oauth',
|
|
14
|
+
'pricing', 'privacy', 'reset-password', 'sign-in', 'sign-up', 'terms',
|
|
15
|
+
]);
|
|
1
16
|
export function isSafeRedirectUrl(url) {
|
|
2
17
|
if (typeof url !== 'string' || !url)
|
|
3
18
|
return false;
|
package/build/tools/approvals.js
CHANGED
|
@@ -42,7 +42,7 @@ export function registerApprovalTools(server, options) {
|
|
|
42
42
|
idempotentHint: true,
|
|
43
43
|
openWorldHint: false,
|
|
44
44
|
},
|
|
45
|
-
description: 'List all pending approval requests for this project. Shows what actions are waiting for human approval.',
|
|
45
|
+
description: 'List all pending approval requests for this project. Excludes expired requests without mutating them. Shows what actions are waiting for human approval.',
|
|
46
46
|
inputSchema: {},
|
|
47
47
|
}, async () => {
|
|
48
48
|
const { data, error } = await db()
|
|
@@ -50,10 +50,11 @@ export function registerApprovalTools(server, options) {
|
|
|
50
50
|
.select('id, action, tier, channel, summary, status, requested_at, expires_at')
|
|
51
51
|
.eq('project_id', getProjectId())
|
|
52
52
|
.eq('status', 'pending')
|
|
53
|
+
.gt('expires_at', new Date().toISOString())
|
|
53
54
|
.order('requested_at', { ascending: false })
|
|
54
55
|
.limit(25);
|
|
55
56
|
if (error)
|
|
56
|
-
return
|
|
57
|
+
return j({ error: error.message, code: 'INTERNAL_ERROR' });
|
|
57
58
|
return j({ pending: data ?? [], count: data?.length ?? 0 });
|
|
58
59
|
});
|
|
59
60
|
}
|
|
@@ -7,7 +7,8 @@ export declare function buildCampaignFiresOn(campaign: Record<string, unknown>):
|
|
|
7
7
|
/**
|
|
8
8
|
* Diagnostic warnings indicating why a campaign's trigger configuration cannot fire (#3852).
|
|
9
9
|
*/
|
|
10
|
-
export declare function buildCampaignTriggerDiagnostics(campaign: Record<string, unknown>): string[];
|
|
10
|
+
export declare function buildCampaignTriggerDiagnostics(campaign: Record<string, unknown>, seenEvents?: ReadonlySet<string> | null): string[];
|
|
11
|
+
export declare function campaignTriggerKind(campaign: Record<string, unknown>): 'event' | 'tag' | 'date_field' | 'contact_event' | 'manual';
|
|
11
12
|
export declare function applyCampaignTargetingFields(updates: Record<string, unknown>, input: {
|
|
12
13
|
trigger_event_name?: string | null;
|
|
13
14
|
trigger_tag?: string | null;
|
package/build/tools/campaigns.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dbErrorResult } from '../lib/db-errors.js';
|
|
1
2
|
import { resolveCampaignReentry, readCampaignPeriods, campaignPeriodFields } from "@sendinel/domain";
|
|
2
3
|
import { CAMPAIGN_BRANCH_CONDITIONS, campaignBranchConditionValue, isContactPropertyConditionList, STEP_PROPERTY_CONDITIONS_ERROR, validateStepPropertyRules } from "@sendinel/domain";
|
|
3
4
|
import { z } from 'zod';
|
|
@@ -105,11 +106,18 @@ export function buildCampaignFiresOn(campaign) {
|
|
|
105
106
|
/**
|
|
106
107
|
* Diagnostic warnings indicating why a campaign's trigger configuration cannot fire (#3852).
|
|
107
108
|
*/
|
|
108
|
-
export function buildCampaignTriggerDiagnostics(campaign) {
|
|
109
|
+
export function buildCampaignTriggerDiagnostics(campaign, seenEvents) {
|
|
109
110
|
const config = (campaign.trigger_config && typeof campaign.trigger_config === 'object' ? campaign.trigger_config : {});
|
|
110
111
|
const events = normalizeTriggerList([...normalizeTriggerList(config.events), campaign.trigger_event_name]);
|
|
111
112
|
const tags = normalizeTriggerList([...normalizeTriggerList(config.tags), campaign.trigger_tag]);
|
|
112
113
|
const diagnostics = [];
|
|
114
|
+
if (events.length && seenEvents === null)
|
|
115
|
+
diagnostics.push('Event history unavailable; prior receipt of these events is unknown.');
|
|
116
|
+
if (seenEvents)
|
|
117
|
+
for (const event of events) {
|
|
118
|
+
if (!seenEvents.has(event))
|
|
119
|
+
diagnostics.push(`Event "${event}" has never been seen in this project; check the event name or send a test event.`);
|
|
120
|
+
}
|
|
113
121
|
if ((events.length || tags.length) && campaign.type !== 'triggered') {
|
|
114
122
|
diagnostics.push(`campaign type is "${String(campaign.type)}"; event and tag triggers only start campaigns of type "triggered" (it will never fire)`);
|
|
115
123
|
}
|
|
@@ -133,6 +141,37 @@ export function buildCampaignTriggerDiagnostics(campaign) {
|
|
|
133
141
|
}
|
|
134
142
|
return diagnostics;
|
|
135
143
|
}
|
|
144
|
+
export function campaignTriggerKind(campaign) {
|
|
145
|
+
if (campaign.trigger_type === 'date_field' || campaign.trigger_type === 'contact_event')
|
|
146
|
+
return campaign.trigger_type;
|
|
147
|
+
const config = normalizeTriggerConfig(campaign.trigger_config);
|
|
148
|
+
if (normalizeTriggerValue(campaign.trigger_event_name) || config.events.length)
|
|
149
|
+
return 'event';
|
|
150
|
+
if (normalizeTriggerValue(campaign.trigger_tag) || config.tags.length)
|
|
151
|
+
return 'tag';
|
|
152
|
+
return 'manual';
|
|
153
|
+
}
|
|
154
|
+
async function campaignTriggerOutput(campaign) {
|
|
155
|
+
let seenEvents;
|
|
156
|
+
const config = normalizeTriggerConfig(campaign.trigger_config);
|
|
157
|
+
if (normalizeTriggerValue(campaign.trigger_event_name) || config.events.length) {
|
|
158
|
+
try {
|
|
159
|
+
// /v1/events -> intakeContactEvent stores event_type=custom and title=event.
|
|
160
|
+
// Read all pages: a capped sample cannot establish that an event was never seen.
|
|
161
|
+
const rows = await readAllRows(db().from('contact_events').select('id, title')
|
|
162
|
+
.eq('project_id', getProjectId()).eq('event_type', 'custom'));
|
|
163
|
+
seenEvents = new Set(rows.map(row => normalizeTriggerValue(row.title)).filter((value) => value !== null));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
seenEvents = null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
trigger_kind: campaignTriggerKind(campaign),
|
|
171
|
+
fires_on: buildCampaignFiresOn(campaign),
|
|
172
|
+
trigger_diagnostics: buildCampaignTriggerDiagnostics(campaign, seenEvents),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
136
175
|
function applyCampaignSendAtField(updates, sendAt, campaignType) {
|
|
137
176
|
if (sendAt === undefined)
|
|
138
177
|
return null;
|
|
@@ -165,13 +204,20 @@ export async function applyCampaignTargetingFields(updates, input, projectId, ex
|
|
|
165
204
|
if (hasTriggerEvent)
|
|
166
205
|
return 'cannot set trigger_event_name on a date_field campaign';
|
|
167
206
|
const config = input.trigger_config ?? {};
|
|
207
|
+
const unknown = Object.keys(config).filter(key => !['field', 'days_before', 'recurring'].includes(key));
|
|
208
|
+
if (unknown.length)
|
|
209
|
+
return `Unknown date_field keys: ${unknown.join(', ')}; allowed: field, days_before, recurring; use days_before, not offset_days`;
|
|
210
|
+
if (config.days_before !== undefined && (typeof config.days_before !== 'number' || !Number.isInteger(config.days_before) || config.days_before < 0 || config.days_before > 30))
|
|
211
|
+
return 'days_before must be an integer from 0 to 30; use days_before, not offset_days';
|
|
212
|
+
if (config.recurring !== undefined && typeof config.recurring !== 'boolean')
|
|
213
|
+
return 'recurring must be a boolean';
|
|
168
214
|
const field = typeof config.field === 'string' ? config.field.trim() : '';
|
|
169
215
|
if (!/^[A-Za-z0-9_.-]{1,80}$/.test(field))
|
|
170
216
|
return 'date field is required';
|
|
171
217
|
updates.trigger_type = 'date_field';
|
|
172
218
|
updates.trigger_config = {
|
|
173
219
|
field,
|
|
174
|
-
days_before:
|
|
220
|
+
days_before: config.days_before ?? 0,
|
|
175
221
|
recurring: config.recurring !== false,
|
|
176
222
|
};
|
|
177
223
|
}
|
|
@@ -197,6 +243,24 @@ export async function applyCampaignTargetingFields(updates, input, projectId, ex
|
|
|
197
243
|
updates.trigger_config = null;
|
|
198
244
|
}
|
|
199
245
|
else {
|
|
246
|
+
const unknown = Object.keys(input.trigger_config).filter(key => !['events', 'tags', 'where'].includes(key));
|
|
247
|
+
if (unknown.length)
|
|
248
|
+
return `Unknown trigger_config keys: ${unknown.join(', ')}; allowed: events, tags, where`;
|
|
249
|
+
if ('where' in input.trigger_config) {
|
|
250
|
+
const where = input.trigger_config.where;
|
|
251
|
+
if (!where || typeof where !== 'object' || Array.isArray(where))
|
|
252
|
+
return 'where must be an object of string, number or boolean values';
|
|
253
|
+
const keys = new Set();
|
|
254
|
+
for (const [key, value] of Object.entries(where)) {
|
|
255
|
+
const normalizedKey = key.trim().replace(/^ctx\./, '');
|
|
256
|
+
if (!normalizedKey || normalizedKey === '__proto__' || keys.has(normalizedKey))
|
|
257
|
+
return `where.${key} has an empty or duplicate normalized key`;
|
|
258
|
+
keys.add(normalizedKey);
|
|
259
|
+
if (!['string', 'number', 'boolean'].includes(typeof value) || (typeof value === 'number' && !Number.isFinite(value))) {
|
|
260
|
+
return `where.${key} must be a string, number or boolean; operators like $gt are not supported`;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
200
264
|
const config = buildCampaignTriggerConfig(input.trigger_config);
|
|
201
265
|
if (!config)
|
|
202
266
|
return 'trigger_config must include events or tags';
|
|
@@ -819,40 +883,56 @@ export function registerCampaignTools(server) {
|
|
|
819
883
|
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.'),
|
|
820
884
|
},
|
|
821
885
|
}, 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 }) => {
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
886
|
+
try {
|
|
887
|
+
const properties = validateStepPropertyRules({ skip_if_property, send_only_if_property });
|
|
888
|
+
if (!properties.ok)
|
|
889
|
+
return j({ error: properties.error, code: "INVALID_INPUT" });
|
|
890
|
+
const { data: campaign, error: campaignError } = await db()
|
|
891
|
+
.schema('email')
|
|
892
|
+
.from('campaigns')
|
|
893
|
+
.select('id')
|
|
894
|
+
.eq('id', campaign_id)
|
|
895
|
+
.eq('project_id', getProjectId())
|
|
896
|
+
.maybeSingle();
|
|
897
|
+
if (campaignError)
|
|
898
|
+
return dbErrorResult(campaignError);
|
|
899
|
+
if (!campaign)
|
|
900
|
+
return { ...j({ error: 'Campaign not found.', code: 'not_found' }), isError: true };
|
|
901
|
+
const renderVars = {};
|
|
902
|
+
if (survey_id) {
|
|
903
|
+
const surveyLink = await resolveSurveyShortLink(db(), getProjectId(), survey_id);
|
|
904
|
+
if (!surveyLink)
|
|
905
|
+
return j({ error: 'Survey has no short link yet', code: 'SURVEY_LINK_NOT_READY' });
|
|
906
|
+
renderVars.survey_link = surveyLink;
|
|
907
|
+
}
|
|
908
|
+
const renderedSubject = survey_id ? renderTemplateVars(subject, renderVars) : subject;
|
|
909
|
+
const normalizedBlocks = blocks ? normalizeComposerBlocks(blocks) : undefined;
|
|
910
|
+
const htmlFromBlocks = normalizedBlocks?.length ? blocksToHtml(normalizedBlocks) : undefined;
|
|
911
|
+
const renderedHtml = body_html !== undefined && survey_id ? renderTemplateVars(body_html, renderVars) : body_html ?? htmlFromBlocks;
|
|
912
|
+
const renderedText = body_text !== undefined && survey_id ? renderTemplateVars(body_text, renderVars) : body_text;
|
|
913
|
+
const { data, error } = await db()
|
|
914
|
+
.from('campaign_steps')
|
|
915
|
+
.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 })
|
|
916
|
+
.select()
|
|
917
|
+
.single();
|
|
918
|
+
if (error)
|
|
919
|
+
return dbErrorResult(error);
|
|
920
|
+
await recordBrandAssetUsage({
|
|
921
|
+
client: db(),
|
|
922
|
+
projectId: getProjectId(),
|
|
923
|
+
source: 'campaign_step',
|
|
924
|
+
resourceId: data.id,
|
|
925
|
+
html: renderedHtml,
|
|
926
|
+
assetIds: brand_asset_ids,
|
|
927
|
+
}).catch((err) => {
|
|
928
|
+
console.warn('Failed to record brand asset usage for MCP campaign step:', err);
|
|
929
|
+
});
|
|
930
|
+
await logAudit({ action: 'campaign.step_created', resourceType: 'campaign_step', resourceId: data.id, details: { property_rules: properties.rules } });
|
|
931
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
932
|
+
}
|
|
933
|
+
catch (error) {
|
|
934
|
+
return dbErrorResult(error);
|
|
831
935
|
}
|
|
832
|
-
const renderedSubject = survey_id ? renderTemplateVars(subject, renderVars) : subject;
|
|
833
|
-
const normalizedBlocks = blocks ? normalizeComposerBlocks(blocks) : undefined;
|
|
834
|
-
const htmlFromBlocks = normalizedBlocks?.length ? blocksToHtml(normalizedBlocks) : undefined;
|
|
835
|
-
const renderedHtml = body_html !== undefined && survey_id ? renderTemplateVars(body_html, renderVars) : body_html ?? htmlFromBlocks;
|
|
836
|
-
const renderedText = body_text !== undefined && survey_id ? renderTemplateVars(body_text, renderVars) : body_text;
|
|
837
|
-
const { data, error } = await db()
|
|
838
|
-
.from('campaign_steps')
|
|
839
|
-
.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 })
|
|
840
|
-
.select()
|
|
841
|
-
.single();
|
|
842
|
-
if (error)
|
|
843
|
-
return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
|
|
844
|
-
await recordBrandAssetUsage({
|
|
845
|
-
client: db(),
|
|
846
|
-
projectId: getProjectId(),
|
|
847
|
-
source: 'campaign_step',
|
|
848
|
-
resourceId: data.id,
|
|
849
|
-
html: renderedHtml,
|
|
850
|
-
assetIds: brand_asset_ids,
|
|
851
|
-
}).catch((err) => {
|
|
852
|
-
console.warn('Failed to record brand asset usage for MCP campaign step:', err);
|
|
853
|
-
});
|
|
854
|
-
await logAudit({ action: 'campaign.step_created', resourceType: 'campaign_step', resourceId: data.id, details: { property_rules: properties.rules } });
|
|
855
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
856
936
|
});
|
|
857
937
|
server.registerTool('update_campaign_status', {
|
|
858
938
|
title: 'Update Campaign Status',
|
|
@@ -3188,8 +3268,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3188
3268
|
send_class: campaign.send_class,
|
|
3189
3269
|
unsub_footer_template: campaign.unsub_footer_template,
|
|
3190
3270
|
...targeting,
|
|
3191
|
-
|
|
3192
|
-
trigger_diagnostics: buildCampaignTriggerDiagnostics({ ...campaign, status: willActivate ? 'active' : 'draft' }),
|
|
3271
|
+
...await campaignTriggerOutput({ ...campaign, status: willActivate ? 'active' : 'draft' }),
|
|
3193
3272
|
steps_created: steps.length,
|
|
3194
3273
|
contacts_enrolled: enrolled,
|
|
3195
3274
|
status: willActivate ? 'active' : 'draft',
|
|
@@ -3381,7 +3460,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3381
3460
|
server.registerTool('set_campaign_trigger', {
|
|
3382
3461
|
title: 'Set Campaign Trigger',
|
|
3383
3462
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
3384
|
-
description: 'Set or clear what a campaign fires on: trigger event name, tag, date_field, contact_event, or trigger_config events/tags and where event-data filter. Bind billing, payment failed, dunning or trial ending mail to an event trigger such as invoice_payment_failed or trial_ending. Drafts save directly; active/paused campaigns require standard human approval. Archived/completed campaigns cannot change. Returns saved trigger fields, fires_on and trigger_diagnostics.',
|
|
3463
|
+
description: 'Set or clear what a campaign fires on: trigger event name, tag, date_field, contact_event, or trigger_config events/tags and where event-data filter. Bind billing, payment failed, dunning or trial ending mail to an event trigger such as invoice_payment_failed or trial_ending. Drafts save directly; active/paused campaigns require standard human approval. Archived/completed campaigns cannot change. Returns saved trigger fields, derived trigger_kind, fires_on and trigger_diagnostics (including unseen-event warnings).',
|
|
3385
3464
|
inputSchema: {
|
|
3386
3465
|
...reentryFields,
|
|
3387
3466
|
campaign_id: z.string().uuid().optional(),
|
|
@@ -3395,7 +3474,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3395
3474
|
}, async ({ approval_id, ...args }) => {
|
|
3396
3475
|
if (approval_id) {
|
|
3397
3476
|
const result = await resumeCampaignEdit('set_campaign_trigger', approval_id, args);
|
|
3398
|
-
return j(result.error ? result : { ...result,
|
|
3477
|
+
return j(result.error ? result : { ...result, ...await campaignTriggerOutput(result) });
|
|
3399
3478
|
}
|
|
3400
3479
|
if (!args.campaign_id)
|
|
3401
3480
|
return j({ error: 'campaign_id is required', code: 'INVALID_FIELD' });
|
|
@@ -3435,7 +3514,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3435
3514
|
if (reentry.period_key !== undefined)
|
|
3436
3515
|
updates.period_key = reentry.period_key;
|
|
3437
3516
|
const result = await finishCampaignEdit('set_campaign_trigger', args, existing, updates, params => requestApproval('set_campaign_trigger', params));
|
|
3438
|
-
return j(result.error || result.requires_approval ? result : { ...result,
|
|
3517
|
+
return j(result.error || result.requires_approval ? result : { ...result, ...await campaignTriggerOutput(result) });
|
|
3439
3518
|
});
|
|
3440
3519
|
server.registerTool('update_campaign', {
|
|
3441
3520
|
title: 'Update Campaign',
|
|
@@ -3468,7 +3547,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3468
3547
|
}, async ({ approval_id, ...input }) => {
|
|
3469
3548
|
if (approval_id) {
|
|
3470
3549
|
const result = await resumeCampaignEdit('update_campaign', approval_id, input);
|
|
3471
|
-
return j(result.error ? result : { ...result,
|
|
3550
|
+
return j(result.error ? result : { ...result, ...await campaignTriggerOutput(result) });
|
|
3472
3551
|
}
|
|
3473
3552
|
const { campaign_id, name, site_id, segment_id, send_at, topic, trigger_event_name, trigger_tag, trigger_type, trigger_config, type, sends_per_hour, send_in_contact_timezone, send_class, unsub_footer_template, reentry_mode, period_key } = input;
|
|
3474
3553
|
if (!campaign_id)
|
|
@@ -3599,7 +3678,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3599
3678
|
idempotentHint: true,
|
|
3600
3679
|
openWorldHint: false,
|
|
3601
3680
|
},
|
|
3602
|
-
description: 'Get full campaign details including all steps, enrollment counts and saved trigger_type, trigger_event_name, trigger_tag, trigger_config (including where filter), fires_on and trigger_diagnostics. Read what event a dunning campaign fires on; use set_campaign_trigger to change it. ' + ACTIVATION_APPROVAL_DESCRIPTION,
|
|
3681
|
+
description: 'Get full campaign details including all steps, enrollment counts and derived trigger_kind (event/tag/date_field/contact_event/manual), saved trigger_type, trigger_event_name, trigger_tag, trigger_config (including where filter), fires_on and trigger_diagnostics. Read what event a dunning campaign fires on; use set_campaign_trigger to change it. ' + ACTIVATION_APPROVAL_DESCRIPTION,
|
|
3603
3682
|
inputSchema: { campaign_id: z.string().uuid(), period: z.string().min(1).optional().describe("Return results for one enrollment period; totals remain all-time.") },
|
|
3604
3683
|
}, async ({ campaign_id, period }) => {
|
|
3605
3684
|
const projectId = getProjectId();
|
|
@@ -3672,8 +3751,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3672
3751
|
...campaign,
|
|
3673
3752
|
reentry_mode: campaign.period_key ? "per_event_period" : "once_ever",
|
|
3674
3753
|
requires_approval_description: ACTIVATION_APPROVAL_DESCRIPTION,
|
|
3675
|
-
|
|
3676
|
-
trigger_diagnostics: buildCampaignTriggerDiagnostics(campaign),
|
|
3754
|
+
...await campaignTriggerOutput(campaign),
|
|
3677
3755
|
total_sent: periods?.totals?.sent ?? sentCount ?? 0,
|
|
3678
3756
|
...(periods?.totals ? { unique_opened: periods.totals.opened, unique_clicked: periods.totals.clicked } : {}),
|
|
3679
3757
|
...campaignPeriodFields(periods, period),
|