@sendinel/mcp-server 1.0.108 → 1.0.109
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/sites.js +55 -9
- package/build/tools/tool-search.js +1 -0
- package/package.json +1 -1
package/build/tools/sites.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { validateSiteUnsubscribeSettings } from '@sendinel/domain';
|
|
3
|
+
import { logAudit } from '../audit.js';
|
|
2
4
|
import { db } from '../db.js';
|
|
3
5
|
import { getProjectId } from '../project.js';
|
|
4
6
|
import { checkPlanLimit } from '../plan-limits.js';
|
|
@@ -112,14 +114,14 @@ export function registerSiteTools(server) {
|
|
|
112
114
|
idempotentHint: true,
|
|
113
115
|
openWorldHint: false,
|
|
114
116
|
},
|
|
115
|
-
description: 'Read the brand voice configuration for a sender/site.',
|
|
117
|
+
description: 'Read the brand voice configuration and current site-wide footer, unsubscribe link settings, unsubscribe text, and physical mailing address for a sender/site.',
|
|
116
118
|
inputSchema: {
|
|
117
119
|
site_id: z.string().uuid(),
|
|
118
120
|
},
|
|
119
121
|
}, async ({ site_id }) => {
|
|
120
122
|
const { data, error } = await db()
|
|
121
123
|
.from('sites')
|
|
122
|
-
.select('id, name, brand_voice')
|
|
124
|
+
.select('id, name, brand_voice, physical_address, unsub_link_enabled, unsub_one_click_enabled, unsub_custom_url, unsub_footer_template, unsub_custom_message, unsub_brand_logo_url, unsub_brand_color')
|
|
123
125
|
.eq('project_id', getProjectId())
|
|
124
126
|
.eq('id', site_id)
|
|
125
127
|
.maybeSingle();
|
|
@@ -184,15 +186,23 @@ export function registerSiteTools(server) {
|
|
|
184
186
|
idempotentHint: true,
|
|
185
187
|
openWorldHint: false,
|
|
186
188
|
},
|
|
187
|
-
description: 'Update
|
|
189
|
+
description: 'Update site-wide email footer, unsubscribe text, unsubscribe link, one-click unsubscribe, unsubscribe page branding, sender name/from address, active status, or physical mailing address. unsub_footer_template must be non-empty and include <unsubscribe>link text</unsubscribe>. Omitted settings and physical_address stay unchanged. Returns stored settings and previous_unsub_footer_template when the footer changes, so callers can undo.',
|
|
188
190
|
inputSchema: {
|
|
189
191
|
site_id: z.string().uuid(),
|
|
190
192
|
name: z.string().optional(),
|
|
191
193
|
from_email: z.string().email().optional(),
|
|
192
194
|
from_name: z.string().optional(),
|
|
193
195
|
active: z.boolean().optional(),
|
|
196
|
+
physical_address: z.string().trim().min(1).optional().describe('Physical mailing address; unchanged unless explicitly supplied.'),
|
|
197
|
+
unsub_link_enabled: z.boolean().optional().describe('Enable the unsubscribe link in the email footer.'),
|
|
198
|
+
unsub_one_click_enabled: z.boolean().optional().describe('Enable one-click unsubscribe headers.'),
|
|
199
|
+
unsub_custom_url: z.string().nullable().optional().describe('HTTP(S) unsubscribe URL; null or blank clears it.'),
|
|
200
|
+
unsub_footer_template: z.string().nullable().optional().describe('Non-empty site-wide footer including <unsubscribe>link text</unsubscribe>.'),
|
|
201
|
+
unsub_custom_message: z.string().nullable().optional().describe('Unsubscribe page message, up to 280 characters; null or blank clears it.'),
|
|
202
|
+
unsub_brand_logo_url: z.string().nullable().optional().describe('HTTP(S) unsubscribe page logo URL; null or blank clears it.'),
|
|
203
|
+
unsub_brand_color: z.string().nullable().optional().describe('Six-digit hex unsubscribe page color; null or blank clears it.'),
|
|
194
204
|
},
|
|
195
|
-
}, async ({ site_id, name, from_email, from_name, active }) => {
|
|
205
|
+
}, async ({ site_id, name, from_email, from_name, active, physical_address, ...unsubscribeInput }) => {
|
|
196
206
|
const updates = {};
|
|
197
207
|
if (name)
|
|
198
208
|
updates.name = name;
|
|
@@ -202,18 +212,54 @@ export function registerSiteTools(server) {
|
|
|
202
212
|
updates.from_name = from_name;
|
|
203
213
|
if (active !== undefined)
|
|
204
214
|
updates.active = active;
|
|
215
|
+
if (physical_address !== undefined)
|
|
216
|
+
updates.physical_address = physical_address.trim().slice(0, 500);
|
|
217
|
+
const unsubscribe = validateSiteUnsubscribeSettings(unsubscribeInput);
|
|
218
|
+
if (!unsubscribe.ok)
|
|
219
|
+
return txt(`Error: ${unsubscribe.error}`);
|
|
220
|
+
Object.assign(updates, unsubscribe.value);
|
|
205
221
|
if (!Object.keys(updates).length)
|
|
206
222
|
return txt('No fields to update.');
|
|
223
|
+
// Read only when a footer is supplied, and guard the write so the undo value is exact.
|
|
224
|
+
const sb = db();
|
|
225
|
+
const projectId = getProjectId();
|
|
226
|
+
let previousTemplate;
|
|
227
|
+
if (updates.unsub_footer_template !== undefined) {
|
|
228
|
+
const { data: current, error } = await sb
|
|
229
|
+
.from('sites')
|
|
230
|
+
.select('unsub_footer_template')
|
|
231
|
+
.eq('id', site_id)
|
|
232
|
+
.eq('project_id', projectId)
|
|
233
|
+
.maybeSingle();
|
|
234
|
+
if (error)
|
|
235
|
+
return txt(`Error: ${error.message}`);
|
|
236
|
+
if (!current)
|
|
237
|
+
return j({ error: 'Site not found', code: 'NOT_FOUND' });
|
|
238
|
+
previousTemplate = current.unsub_footer_template;
|
|
239
|
+
}
|
|
207
240
|
updates.updated_at = new Date().toISOString();
|
|
208
|
-
|
|
241
|
+
let query = sb
|
|
209
242
|
.from('sites')
|
|
210
243
|
.update(updates)
|
|
211
244
|
.eq('id', site_id)
|
|
212
|
-
.eq('project_id',
|
|
213
|
-
|
|
214
|
-
.
|
|
245
|
+
.eq('project_id', projectId);
|
|
246
|
+
if (previousTemplate === null)
|
|
247
|
+
query = query.is('unsub_footer_template', null);
|
|
248
|
+
else if (previousTemplate !== undefined)
|
|
249
|
+
query = query.eq('unsub_footer_template', previousTemplate);
|
|
250
|
+
const { data, error } = await query.select().maybeSingle();
|
|
215
251
|
if (error)
|
|
216
252
|
return txt(`Error: ${error.message}`);
|
|
217
|
-
|
|
253
|
+
if (!data)
|
|
254
|
+
return j({ error: 'Site not found or footer changed during update. Read the site and retry.', code: 'SITE_CHANGED_OR_NOT_FOUND' });
|
|
255
|
+
await logAudit({
|
|
256
|
+
action: 'site.updated', resourceType: 'site', resourceId: site_id,
|
|
257
|
+
details: { fields: Object.keys(updates).filter((field) => field !== 'updated_at') },
|
|
258
|
+
});
|
|
259
|
+
return j({
|
|
260
|
+
...data,
|
|
261
|
+
...(previousTemplate !== undefined && previousTemplate !== data.unsub_footer_template
|
|
262
|
+
? { previous_unsub_footer_template: previousTemplate } : {}),
|
|
263
|
+
});
|
|
218
264
|
});
|
|
219
265
|
}
|
|
@@ -12,6 +12,7 @@ export const STOPWORDS = new Set([
|
|
|
12
12
|
* Only tools that exist in the registry belong here.
|
|
13
13
|
*/
|
|
14
14
|
export const TOOL_ALIASES = {
|
|
15
|
+
update_site: ['footer', 'email footer', 'unsubscribe text', 'unsubscribe link', 'site footer', 'unsubscribe settings'],
|
|
15
16
|
set_campaign_trigger: ['trigger', 'event', 'fires on', 'fires-on', 'dunning', 'set campaign trigger event name', 'invoice payment failed', 'trial ending'],
|
|
16
17
|
send_broadcast: [
|
|
17
18
|
'send email', 'send an email', 'email blast', 'blast', 'newsletter', 'announce', 'announcement', 'one-off email',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.109",
|
|
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",
|