@sendinel/mcp-server 1.0.183 → 1.0.184
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,5 @@
|
|
|
1
1
|
/** Hand-written workflows, checked against the registered schemas in CI (#4061). */
|
|
2
|
+
import { SEGMENT_OPERATORS_DESCRIPTION } from '@sendinel/domain';
|
|
2
3
|
import { rankTools, scoreTool, tokenize, TOOL_ALIASES } from '../tools/tool-search.js';
|
|
3
4
|
const approval = { required: true, how: 'returns approve_url; re-call with approval_id' };
|
|
4
5
|
const from = (n, field) => `<from step ${n}: ${field}>`;
|
|
@@ -103,21 +104,48 @@ export const HOW_TO_RECIPES = [
|
|
|
103
104
|
gotchas: [projectRule, 'No recorded bounce is not proof that an email arrived. Do not remove suppressions as a diagnostic step. Broader diagnose_delivery_issue requires write scope despite its read-only annotation.'],
|
|
104
105
|
done_when: 'Explain the recorded bounce evidence and next action, or explicitly state that no matching bounce was recorded.',
|
|
105
106
|
},
|
|
107
|
+
{
|
|
108
|
+
id: 'build-a-segment', title: 'Build a segment',
|
|
109
|
+
when_to_use: 'Create a saved audience segment with explicit rules and preview its membership.',
|
|
110
|
+
synonyms: ['build a segment', 'create segment', 'segment rules', 'audience filters'],
|
|
111
|
+
requires: { scopes: ['write'], tester_ok: false },
|
|
112
|
+
steps: [
|
|
113
|
+
{ n: 1, tool: 'create_segment', args_example: { name: 'Engaged customers', rules: { op: 'AND', conditions: [{ field: 'lifecycle_stage', operator: 'eq', value: 'customer' }, { field: 'engagement_score', operator: 'gte', value: 50 }] } }, note: `Worked example: customers with engagement_score >= 50. ${SEGMENT_OPERATORS_DESCRIPTION}` },
|
|
114
|
+
{ n: 2, tool: 'preview_segment', args_example: { segment_id: from(1, 'id') }, note: 'Review total_contacts and the sample; create_segment also returns estimated_contacts. Counts are dynamic and are not proof of send eligibility.' },
|
|
115
|
+
],
|
|
116
|
+
gotchas: [projectRule, 'Use match_all:true explicitly for everyone. Empty or malformed rules are rejected. Preview does not send or enroll anyone.'],
|
|
117
|
+
done_when: 'The saved rules and preview match the intended audience.',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'draft-a-template', title: 'Draft a template',
|
|
121
|
+
when_to_use: 'Save supplied email content as a reusable template, preview it and copy it into a campaign step.',
|
|
122
|
+
synonyms: ['draft a template', 'create template', 'template to campaign', 'use a template in a campaign'],
|
|
123
|
+
requires: { scopes: ['write'], tester_ok: false },
|
|
124
|
+
steps: [sender,
|
|
125
|
+
{ n: 2, tool: 'create_template', args_example: { name: 'Product update', brief: 'Reusable product update', subject_hint: 'Your product update', body_html: '<p>Here is your product update.</p>' }, note: 'brief is required even with HTML; it is stored as a description/label without AI generation or AI credits. For AI-generated content use create_template_from_brief instead.' },
|
|
126
|
+
{ n: 3, tool: 'get_template', args_example: { id: from(2, 'template.id') }, note: 'Read the saved subject_hint and body_html.' },
|
|
127
|
+
{ 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.' },
|
|
128
|
+
{ n: 5, tool: 'list_templates', args_example: { search: 'Product update' }, note: 'Verify the reusable template is discoverable; paginate if truncated.' },
|
|
129
|
+
{ 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.' },
|
|
130
|
+
{ 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: 'Copy content from get_template. No template_id parameter exists here; template_id/template_vars in the response are reserved for this path. This draft is not activated.' },
|
|
131
|
+
],
|
|
132
|
+
gotchas: [projectRule, previewRule, 'Later template edits do not update copied step content. 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.'],
|
|
133
|
+
done_when: 'The template is saved, previewed and listed; if requested, its content is copied into an unactivated campaign step.',
|
|
134
|
+
},
|
|
106
135
|
{
|
|
107
136
|
id: 'campaign-build-launch', title: 'Build and launch an email campaign',
|
|
108
|
-
when_to_use: '
|
|
109
|
-
synonyms: ['build campaign', 'draft campaign', 'campaign build launch', 'campaign from brief'],
|
|
137
|
+
when_to_use: 'Review and schedule a one-off email campaign for a saved segment.',
|
|
138
|
+
synonyms: ['build campaign', 'draft campaign', 'campaign build launch', 'campaign from brief', 'schedule a campaign for a segment'],
|
|
110
139
|
requires: { scopes: ['write'], tester_ok: false },
|
|
111
140
|
steps: [sender,
|
|
112
|
-
{ n: 2, tool: '
|
|
113
|
-
{ n: 3, tool: '
|
|
114
|
-
{ n: 4, tool: '
|
|
115
|
-
{ n: 5, tool: '
|
|
116
|
-
{ n: 6, tool: '
|
|
117
|
-
{ n: 7, tool: 'activate_campaigns', args_example: { approval_id: from(6, 'approval_id') }, approval, note: 'ONLY approval_id after human approval.' },
|
|
141
|
+
{ n: 2, tool: 'list_segments', args_example: {}, note: 'Choose the intended saved segment.' },
|
|
142
|
+
{ n: 3, tool: 'preview_segment', args_example: { segment_id: from(2, '0.id') }, note: 'Review membership; actual eligible recipients are resolved by send_broadcast.' },
|
|
143
|
+
{ n: 4, tool: 'preview_email', args_example: { site_id: from(1, '0.id'), subject: 'Your update', body_html: '<p>Your reviewed update.</p>' }, note: 'Review the real renderer without sending.' },
|
|
144
|
+
{ n: 5, tool: 'send_broadcast', args_example: { site_id: from(1, '0.id'), segment_id: from(2, '0.id'), subject: 'Your update', html: '<p>Your reviewed update.</p>' }, approval, note: 'To schedule, add send_at with the requested future ISO instant. Show the finished content, eligible recipient count, sender, schedule and approve_url; wait for human approval. This call sends nothing.' },
|
|
145
|
+
{ n: 6, tool: 'send_broadcast', args_example: { approval_id: from(5, 'approval_id') }, approval, note: 'After approval pass ONLY approval_id. Creates, enrolls and activates the approved broadcast.' },
|
|
118
146
|
],
|
|
119
|
-
gotchas: [projectRule, previewRule, '
|
|
120
|
-
done_when: '
|
|
147
|
+
gotchas: [projectRule, previewRule, 'For saved sequences/journeys use create_campaign, add_campaign_step and activate_campaigns with human approval; activation alone enrolls nobody. broadcast_to_segment is the immediate segment broadcast alternative. launch_campaign builds a NEW campaign from briefs.'],
|
|
148
|
+
done_when: 'Approved execution reports the intended broadcast queued or scheduled; actual delivery requires separate verification.',
|
|
121
149
|
},
|
|
122
150
|
{
|
|
123
151
|
id: 'list-hygiene', title: 'Audit list hygiene and duplicates',
|
|
@@ -281,7 +281,7 @@ export function registerCampaignSetTools(server) {
|
|
|
281
281
|
server.registerTool('activate_campaigns', {
|
|
282
282
|
title: 'Activate Campaigns',
|
|
283
283
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
284
|
-
description: 'Turn on a set of draft or paused campaigns behind one human approval. Call 1: give campaign_set_id or campaign_ids; it returns one approval_id with a summary of every campaign, and activates nothing. After a person approves (approve_url works on a phone), call again with only approval_id. It refuses if any campaign or email changed after approval. Activating a broadcast enrolls nobody; use send_broadcast or broadcast_to_segment to create, enroll and activate a broadcast for sending.',
|
|
284
|
+
description: 'Turn on a set of draft or paused campaigns behind one human approval. Call 1: give campaign_set_id or campaign_ids; it returns one approval_id with a summary of every campaign, and activates nothing. After a person approves (approve_url works on a phone), call again with only approval_id. It refuses if any campaign or email changed after approval. Use this for saved sequences/journeys. To schedule a one-off segment broadcast use send_broadcast with segment_id and send_at; its content and audience require approval. Activating a broadcast enrolls nobody; use send_broadcast or broadcast_to_segment to create, enroll and activate a broadcast for sending.',
|
|
285
285
|
inputSchema: {
|
|
286
286
|
campaign_set_id: z.string().uuid().optional(),
|
|
287
287
|
campaign_ids: z.array(z.string().uuid()).min(1).max(50).optional(),
|
package/build/tools/campaigns.js
CHANGED
|
@@ -886,7 +886,7 @@ export function registerCampaignTools(server) {
|
|
|
886
886
|
idempotentHint: false,
|
|
887
887
|
openWorldHint: false,
|
|
888
888
|
},
|
|
889
|
-
description: 'Create a new email campaign. For triggered campaigns, provide one trigger shape: trigger_event_name, trigger_tag, trigger_type=date_field with trigger_config, or segment_id for ongoing audience targeting. For type=broadcast, send_at schedules the send for a future instant; the
|
|
889
|
+
description: 'Create a new email campaign. For triggered campaigns, provide one trigger shape: trigger_event_name, trigger_tag, trigger_type=date_field with trigger_config, or segment_id for ongoing audience targeting. For type=broadcast, send_at schedules the send for a future instant; the draft still needs activation via activate_campaigns (human approval), which enrolls nobody. Use create_campaign plus steps and activate_campaigns for saved sequences/journeys; for a one-off segment broadcast use send_broadcast with segment_id and optional send_at, which creates, enrolls and activates after approval.',
|
|
890
890
|
inputSchema: {
|
|
891
891
|
...reentryFields,
|
|
892
892
|
site_id: z.string().uuid(),
|
|
@@ -900,7 +900,7 @@ export function registerCampaignTools(server) {
|
|
|
900
900
|
trigger_type: z.enum(['date_field']).optional().describe('Set for elapsed-time triggers keyed on a contact custom_properties date field'),
|
|
901
901
|
trigger_config: z.record(z.string(), z.unknown()).nullable().optional().describe('For trigger_type=date_field: { field, days_before?, recurring? }. Otherwise: { events?: string[], tags?: string[] }.'),
|
|
902
902
|
segment_id: z.string().uuid().nullable().optional().describe('Segment UUID for ongoing audience targeting; membership is re-evaluated on sends'),
|
|
903
|
-
send_at: z.string().datetime().nullable().optional().describe('For type=broadcast only: ISO 8601 instant to schedule the send. Must be in the future. Set null for no schedule.
|
|
903
|
+
send_at: z.string().datetime().nullable().optional().describe('For type=broadcast only: ISO 8601 instant to schedule the send. Must be in the future. Set null for no schedule. Activation via activate_campaigns requires human approval and enrolls nobody. To schedule a one-off segment broadcast use send_broadcast with segment_id and send_at.'),
|
|
904
904
|
send_class: z.enum(['broadcast', 'product']).optional().describe("Who may receive it when the project's consent gate is on: 'broadcast' (default) = only contacts who opted in to marketing; 'product' = also the customer's signed-up users (onboarding, trial, account mail)."),
|
|
905
905
|
},
|
|
906
906
|
}, async ({ site_id, name, type, description, sends_per_hour, requires_approval, trigger_event_name, trigger_tag, trigger_type, trigger_config, segment_id, send_at, send_class, reentry_mode, period_key }) => {
|
|
@@ -952,7 +952,7 @@ export function registerCampaignTools(server) {
|
|
|
952
952
|
idempotentHint: false,
|
|
953
953
|
openWorldHint: false,
|
|
954
954
|
},
|
|
955
|
-
description: 'Add an email step to a campaign. Requires a non-blank subject and body_html, body_text, or blocks that render to content. For A/B testing, add multiple steps with the same step_order but different variant_label values.',
|
|
955
|
+
description: 'Add an email step to a campaign. Requires a non-blank subject and body_html, body_text, or blocks that render to content. To use a stored template, call get_template and copy subject_hint into subject and body_html into body_html (and body_text if available). This tool accepts no template_id or template_vars; those response fields are reserved here and are not set by this tool. For A/B testing, add multiple steps with the same step_order but different variant_label values.',
|
|
956
956
|
inputSchema: z.object({
|
|
957
957
|
campaign_id: z.string().uuid(),
|
|
958
958
|
step_order: z.number().int().min(0).describe('Position in sequence (0 = first)'),
|
|
@@ -1326,7 +1326,7 @@ export function registerCampaignTools(server) {
|
|
|
1326
1326
|
idempotentHint: false,
|
|
1327
1327
|
openWorldHint: true,
|
|
1328
1328
|
},
|
|
1329
|
-
description: 'Create a one-email broadcast campaign for an existing segment, enroll eligible contacts immediately, and activate it. Uses two-call confirmation plus approval before sending to real contacts.',
|
|
1329
|
+
description: 'Create a one-email broadcast campaign for an existing segment, enroll eligible contacts immediately, and activate it. Uses two-call confirmation plus approval before sending to real contacts. Use this for an immediate one-off segment broadcast; for a scheduled segment broadcast use send_broadcast with segment_id and send_at. For sequences/journeys use create_campaign, add_campaign_step and activate_campaigns.',
|
|
1330
1330
|
inputSchema: {
|
|
1331
1331
|
site_id: z.string().uuid().describe('Site/sender to send from'),
|
|
1332
1332
|
segment_id: z.string().uuid().describe('Existing segment to target'),
|
|
@@ -1488,7 +1488,7 @@ export function registerCampaignTools(server) {
|
|
|
1488
1488
|
idempotentHint: false,
|
|
1489
1489
|
openWorldHint: true,
|
|
1490
1490
|
},
|
|
1491
|
-
description: 'Send a one-off email in two calls. Call 1: give an audience (audience in plain words, or segment_id) and content (brief for an AI draft, or html + subject); it returns the finished email, the recipient count, the sender and an approval_id, and sends nothing. A person approves it (approve_url works on a phone). Call 2: pass ONLY approval_id; it sends exactly what was approved. Use this for
|
|
1491
|
+
description: 'Send a one-off email in two calls. Call 1: give an audience (audience in plain words, or segment_id) and content (brief for an AI draft, or html + subject); it returns the finished email, the recipient count, the sender and an approval_id, and sends nothing. A person approves it (approve_url works on a phone). Call 2: pass ONLY approval_id; it sends exactly what was approved. Use this for one-off segment broadcasts: segment_id plus optional send_at schedules the campaign after approval of the exact content and audience. For sequences/journeys use create_campaign, add_campaign_step and activate_campaigns.',
|
|
1492
1492
|
inputSchema: {
|
|
1493
1493
|
audience: z.string().min(3).max(500).optional().describe('Who gets it, in plain words (for example "trial users who have not opened anything"). Use this or segment_id.'),
|
|
1494
1494
|
segment_id: z.string().uuid().optional().describe('A saved segment. Use this or audience.'),
|
package/build/tools/how-to.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { requestContext } from '../db.js';
|
|
3
|
+
import { LIMITED_GRANT_DENIED_TOOLS } from '../lib/limited-grant.js';
|
|
2
4
|
import { HOW_TO_RECIPES, matchRecipes } from '../lib/how-to-recipes.js';
|
|
3
5
|
import { logHowToCall } from '../lib/how-to-usage.js';
|
|
4
6
|
import { getRegisteredToolGroups, isTesterConnection } from '../server.js';
|
|
@@ -14,7 +16,11 @@ export function registerHowTo(server, options = {}) {
|
|
|
14
16
|
},
|
|
15
17
|
}, async ({ goal, limit }) => {
|
|
16
18
|
const scopes = options.scopes?.filter((s) => s !== 'gateway');
|
|
17
|
-
const
|
|
19
|
+
const registered = getRegisteredToolGroups({ ...options, scopes });
|
|
20
|
+
const limited = requestContext()?.allowedProjectIds != null;
|
|
21
|
+
const groups = limited
|
|
22
|
+
? Object.fromEntries(Object.entries(registered).filter(([name]) => !LIMITED_GRANT_DENIED_TOOLS.has(name)))
|
|
23
|
+
: registered;
|
|
18
24
|
const available = HOW_TO_RECIPES.filter((recipe) => (!isTesterConnection(scopes) || recipe.requires.tester_ok)
|
|
19
25
|
&& recipe.steps.every((step) => Object.hasOwn(groups, step.tool)));
|
|
20
26
|
const tools = Object.entries(groups).map(([name, group]) => ({ name, group, description: '' }));
|
package/build/tools/segments.js
CHANGED
|
@@ -7,7 +7,7 @@ import { getProjectId } from '../project.js';
|
|
|
7
7
|
import { dbErrorResult, dbToolError, toolError } from '../lib/db-error.js';
|
|
8
8
|
import { getAnthropicModelId } from '../anthropic-model.js';
|
|
9
9
|
import { AI_MAX_TOKENS, outputConfig, anthropicCheckedText } from '@sendinel/domain';
|
|
10
|
-
import { applySegmentRules, buildSegmentBaseQuery, evaluateSegmentRules, hydrateEmailSendsContacts, isSegmentRulesError, normalizeSegmentRulesToTree, prepareSegmentRulesForStorage, readAllRows, } from '@sendinel/domain';
|
|
10
|
+
import { SEGMENT_OPERATORS_DESCRIPTION, applySegmentRules, buildSegmentBaseQuery, evaluateSegmentRules, hydrateEmailSendsContacts, isSegmentRulesError, normalizeSegmentRulesToTree, prepareSegmentRulesForStorage, readAllRows, } from '@sendinel/domain';
|
|
11
11
|
async function loadFieldTypes(projectId) {
|
|
12
12
|
const client = db();
|
|
13
13
|
const scoped = typeof client.schema === 'function' ? client.schema('email') : client;
|
|
@@ -77,7 +77,7 @@ export function registerSegmentTools(server) {
|
|
|
77
77
|
idempotentHint: false,
|
|
78
78
|
openWorldHint: false,
|
|
79
79
|
},
|
|
80
|
-
description:
|
|
80
|
+
description: `Create a saved segment with filter rules. Segments evaluate dynamically at send time. ${SEGMENT_OPERATORS_DESCRIPTION}`,
|
|
81
81
|
inputSchema: {
|
|
82
82
|
name: z.string().min(1).describe('Segment name (unique per project)'),
|
|
83
83
|
description: z.string().optional(),
|
|
@@ -160,7 +160,7 @@ export function registerSegmentTools(server) {
|
|
|
160
160
|
idempotentHint: true,
|
|
161
161
|
openWorldHint: false,
|
|
162
162
|
},
|
|
163
|
-
description:
|
|
163
|
+
description: `Update a segment name, description, or rules. ${SEGMENT_OPERATORS_DESCRIPTION}`,
|
|
164
164
|
inputSchema: {
|
|
165
165
|
segment_id: z.string().uuid(),
|
|
166
166
|
name: z.string().optional(),
|
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: 'Save an email template from supplied content. brief is a required description/label stored as brief_template, even with body_html; this tool performs no AI generation and consumes no AI credits. Use create_template_from_brief to AI-generate content using the site brand voice.',
|
|
245
245
|
inputSchema: {
|
|
246
246
|
name: templateName,
|
|
247
|
-
brief: z.string().min(1),
|
|
247
|
+
brief: z.string().min(1).describe('Required description/label; stored only, no AI generation or AI credits.'),
|
|
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.184",
|
|
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",
|