@sendinel/mcp-server 1.0.176 → 1.0.178
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/db.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export declare function runWithClient<T>(client: any, fn: () => Promise<T>, requ
|
|
|
41
41
|
actorId?: string | null;
|
|
42
42
|
oauthTokenId?: string | null;
|
|
43
43
|
chatConfirmEnabled?: boolean;
|
|
44
|
+
allowedProjectIds?: string[] | null;
|
|
44
45
|
invitationProjectIds?: string[] | null;
|
|
45
46
|
authKind?: string | null;
|
|
46
47
|
oauthClientId?: string | null;
|
|
@@ -68,6 +69,7 @@ export declare function requestContext(): {
|
|
|
68
69
|
actorId?: string | null;
|
|
69
70
|
oauthTokenId?: string | null;
|
|
70
71
|
chatConfirmEnabled?: boolean;
|
|
72
|
+
allowedProjectIds?: string[] | null;
|
|
71
73
|
invitationProjectIds?: string[] | null;
|
|
72
74
|
authKind?: string | null;
|
|
73
75
|
oauthClientId?: string | null;
|
package/build/db.js
CHANGED
|
@@ -80,6 +80,7 @@ export function requestContext() {
|
|
|
80
80
|
oauthTokenId: ctx.oauthTokenId,
|
|
81
81
|
chatConfirmEnabled: ctx.chatConfirmEnabled,
|
|
82
82
|
invitationProjectIds: ctx.invitationProjectIds,
|
|
83
|
+
allowedProjectIds: ctx.allowedProjectIds,
|
|
83
84
|
authKind: ctx.authKind,
|
|
84
85
|
oauthClientId: ctx.oauthClientId,
|
|
85
86
|
agentLabel: ctx.agentLabel,
|
|
@@ -25,6 +25,7 @@ export declare function chatConfirmConnectionEnabled(identity?: {
|
|
|
25
25
|
actorId?: string | null;
|
|
26
26
|
oauthTokenId?: string | null;
|
|
27
27
|
chatConfirmEnabled?: boolean;
|
|
28
|
+
allowedProjectIds?: string[] | null;
|
|
28
29
|
invitationProjectIds?: string[] | null;
|
|
29
30
|
authKind?: string | null;
|
|
30
31
|
oauthClientId?: string | null;
|
package/build/tools/analytics.js
CHANGED
|
@@ -309,6 +309,8 @@ export function registerAnalyticsTools(server) {
|
|
|
309
309
|
return txt(`Error: ${dbToolError(ownedProjectsError).error}`);
|
|
310
310
|
const byId = new Map();
|
|
311
311
|
for (const project of [...(directProjects ?? []), ...(ownedProjects ?? [])]) {
|
|
312
|
+
if (ctx.allowedProjectIds != null && !ctx.allowedProjectIds.includes(project.id))
|
|
313
|
+
continue;
|
|
312
314
|
if (ctx.invitationProjectIds != null && !ctx.invitationProjectIds.includes(project.id))
|
|
313
315
|
continue;
|
|
314
316
|
byId.set(project.id, project);
|
package/build/tools/templates.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { withCreatedItems } from '../lib/mcp-created-items.js';
|
|
2
|
-
import { literalIlikePattern } from '@sendinel/domain';
|
|
2
|
+
import { literalIlikePattern, refuseActiveText, findUnresolvedTemplateTags } from '@sendinel/domain';
|
|
3
3
|
import { withApproval } from '../lib/action-approvals.js';
|
|
4
4
|
import { snapshotTemplateVersion, versionHasBodySnapshot } from '@sendinel/domain';
|
|
5
5
|
import { z } from 'zod';
|
|
@@ -8,9 +8,20 @@ import { db, requestContext } from '../db.js';
|
|
|
8
8
|
import { getProjectId } from '../project.js';
|
|
9
9
|
import { recordBrandAssetUsage } from '../lib/brand-asset-usage.js';
|
|
10
10
|
import { deriveTemplateVariables } from '../lib/voice.js';
|
|
11
|
-
import { dbToolError } from '../lib/db-error.js';
|
|
11
|
+
import { dbToolError, toolError } from '../lib/db-error.js';
|
|
12
|
+
import { normalizeTagInput } from '../lib/tags.js';
|
|
13
|
+
// campaign_templates.name is TEXT NOT NULL; enforce the tool's name limit here.
|
|
14
|
+
const templateName = z.string().trim().min(1).max(200);
|
|
12
15
|
const txt = (text) => ({ content: [{ type: 'text', text }] });
|
|
13
16
|
const j = (data) => txt(JSON.stringify(data, null, 2));
|
|
17
|
+
function templateContentError(content) {
|
|
18
|
+
return refuseActiveText('body_html', content.body_html) ?? refuseActiveText('subject_hint', content.subject_hint);
|
|
19
|
+
}
|
|
20
|
+
function templateWarnings(content) {
|
|
21
|
+
const tags = new Set([content.body_html, content.subject_hint]
|
|
22
|
+
.flatMap(value => typeof value === 'string' ? findUnresolvedTemplateTags(value) : []));
|
|
23
|
+
return [...tags].map(tag => `Unresolved template tag: ${tag}`);
|
|
24
|
+
}
|
|
14
25
|
function stripHtml(value) {
|
|
15
26
|
return value
|
|
16
27
|
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
@@ -39,17 +50,15 @@ function translateMergeTags(body) {
|
|
|
39
50
|
variables.add('unsubscribe_url');
|
|
40
51
|
return '{{unsubscribe_url}}';
|
|
41
52
|
})
|
|
42
|
-
.replace(/\*\|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
variables.add('email');
|
|
52
|
-
return '{{email}}';
|
|
53
|
+
.replace(/\*\|([^|]*?)\|\*/g, (literal, tag) => {
|
|
54
|
+
const mapped = { FNAME: 'first_name', LNAME: 'last_name', EMAIL: 'email', UNSUB: 'unsubscribe_url' };
|
|
55
|
+
const key = mapped[tag.toUpperCase()];
|
|
56
|
+
if (!key) {
|
|
57
|
+
unmapped.add(literal);
|
|
58
|
+
return literal;
|
|
59
|
+
}
|
|
60
|
+
variables.add(key);
|
|
61
|
+
return `{{${key}}}`;
|
|
53
62
|
})
|
|
54
63
|
.replace(/{{\s*customer\.([a-zA-Z0-9_.-]+)(?:\s*\|[^}]*)?\s*}}/g, (_match, token) => {
|
|
55
64
|
const name = `custom.${String(token).replace(/^custom\./, '')}`;
|
|
@@ -57,12 +66,8 @@ function translateMergeTags(body) {
|
|
|
57
66
|
unmapped.add(`customer.${token}`);
|
|
58
67
|
return `{{${name}}}`;
|
|
59
68
|
});
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
while ((match = tokenPattern.exec(translated)) !== null) {
|
|
63
|
-
if (match[1])
|
|
64
|
-
variables.add(match[1]);
|
|
65
|
-
}
|
|
69
|
+
for (const variable of deriveTemplateVariables({ body_html: translated }))
|
|
70
|
+
variables.add(variable);
|
|
66
71
|
return { body: translated, variables: Array.from(variables), unmapped: Array.from(unmapped) };
|
|
67
72
|
}
|
|
68
73
|
export function registerTemplateTools(server) {
|
|
@@ -77,17 +82,19 @@ export function registerTemplateTools(server) {
|
|
|
77
82
|
description: 'List email templates for the current project, including reusable system templates.',
|
|
78
83
|
inputSchema: {
|
|
79
84
|
limit: z.number().int().min(1).max(100).default(50).optional(),
|
|
85
|
+
offset: z.number().int().min(0).default(0).optional(),
|
|
80
86
|
category: z.string().optional(),
|
|
81
87
|
search: z.string().optional(),
|
|
82
88
|
},
|
|
83
|
-
}, async ({ limit = 50, category, search }) => {
|
|
89
|
+
}, async ({ limit = 50, offset = 0, category, search }) => {
|
|
84
90
|
const projectId = getProjectId();
|
|
85
91
|
let query = db().from('campaign_templates')
|
|
86
92
|
.select('id, name, category, description, subject_hint, brief_template, body_html, variables, tags, project_id, created_at, updated_at, is_transactional')
|
|
87
93
|
.eq('is_archived', false)
|
|
88
94
|
.or(`project_id.is.null,project_id.eq.${projectId}`)
|
|
89
95
|
.order('created_at', { ascending: false })
|
|
90
|
-
.
|
|
96
|
+
.order('id', { ascending: false })
|
|
97
|
+
.range(offset, offset + limit);
|
|
91
98
|
if (category)
|
|
92
99
|
query = query.eq('category', category);
|
|
93
100
|
if (search)
|
|
@@ -95,7 +102,9 @@ export function registerTemplateTools(server) {
|
|
|
95
102
|
const { data, error } = await query;
|
|
96
103
|
if (error)
|
|
97
104
|
return j(dbToolError(error));
|
|
98
|
-
|
|
105
|
+
const templates = (data ?? []).slice(0, limit);
|
|
106
|
+
const truncated = (data?.length ?? 0) > limit;
|
|
107
|
+
return j({ templates, count: templates.length, truncated, next_offset: truncated ? offset + limit : null });
|
|
99
108
|
});
|
|
100
109
|
server.registerTool('translate_template', {
|
|
101
110
|
title: 'Translate Template',
|
|
@@ -125,11 +134,12 @@ export function registerTemplateTools(server) {
|
|
|
125
134
|
if (!source)
|
|
126
135
|
return j({ error: 'html or text is required' });
|
|
127
136
|
const translatedBody = translateMergeTags(source);
|
|
137
|
+
const translatedText = text !== undefined ? translateMergeTags(text) : null;
|
|
128
138
|
const translatedSubject = translateMergeTags(String(subject ?? name ?? 'Imported template'));
|
|
129
139
|
const bodyHtml = html ? translatedBody.body : null;
|
|
130
|
-
const bodyText =
|
|
131
|
-
const variables = Array.from(new Set([...translatedBody.variables, ...translatedSubject.variables]));
|
|
132
|
-
const unmapped = Array.from(new Set([...translatedBody.unmapped, ...translatedSubject.unmapped]));
|
|
140
|
+
const bodyText = translatedText?.body ?? stripHtml(translatedBody.body);
|
|
141
|
+
const variables = Array.from(new Set([...translatedBody.variables, ...(translatedText?.variables ?? []), ...translatedSubject.variables]));
|
|
142
|
+
const unmapped = Array.from(new Set([...translatedBody.unmapped, ...(translatedText?.unmapped ?? []), ...translatedSubject.unmapped]));
|
|
133
143
|
const translated = {
|
|
134
144
|
provider,
|
|
135
145
|
source_template_id: source_template_id ?? null,
|
|
@@ -213,12 +223,14 @@ export function registerTemplateTools(server) {
|
|
|
213
223
|
}, async ({ id }) => {
|
|
214
224
|
const projectId = getProjectId();
|
|
215
225
|
const { data, error } = await db().from('campaign_templates')
|
|
216
|
-
.select('id, name, category, description, subject_hint, brief_template, body_html, variables, tags, project_id, created_at, updated_at, is_transactional')
|
|
226
|
+
.select('id, name, category, description, subject_hint, brief_template, body_html, variables, tags, project_id, created_at, updated_at, is_transactional, is_archived')
|
|
217
227
|
.eq('id', id)
|
|
218
228
|
.or(`project_id.is.null,project_id.eq.${projectId}`)
|
|
219
229
|
.maybeSingle();
|
|
220
230
|
if (error)
|
|
221
231
|
return j(dbToolError(error));
|
|
232
|
+
if (!data)
|
|
233
|
+
return toolError({ error: 'Template not found', code: 'not_found' });
|
|
222
234
|
return j({ template: data });
|
|
223
235
|
});
|
|
224
236
|
server.registerTool('create_template', {
|
|
@@ -231,7 +243,7 @@ export function registerTemplateTools(server) {
|
|
|
231
243
|
},
|
|
232
244
|
description: 'Create a new email template. Use create_template_from_brief to AI-generate the content.',
|
|
233
245
|
inputSchema: {
|
|
234
|
-
name:
|
|
246
|
+
name: templateName,
|
|
235
247
|
brief: z.string().min(1),
|
|
236
248
|
subject_hint: z.string().optional(),
|
|
237
249
|
body_html: z.string().optional(),
|
|
@@ -242,18 +254,25 @@ export function registerTemplateTools(server) {
|
|
|
242
254
|
brand_asset_ids: z.array(z.string().uuid()).optional().describe('Stable brand_media_assets ids explicitly referenced by this template.'),
|
|
243
255
|
},
|
|
244
256
|
}, async ({ name, brief, subject_hint, body_html, category, description, tags, is_transactional, brand_asset_ids }) => {
|
|
257
|
+
const content = { body_html, subject_hint };
|
|
258
|
+
const contentError = templateContentError(content);
|
|
259
|
+
if (contentError)
|
|
260
|
+
return toolError({ error: contentError, code: 'validation_failed' });
|
|
261
|
+
const parsedName = templateName.safeParse(name);
|
|
262
|
+
if (!parsedName.success)
|
|
263
|
+
return toolError({ error: 'Name must contain 1 to 200 characters after trimming', code: 'validation_failed' });
|
|
245
264
|
const projectId = getProjectId();
|
|
246
265
|
const { data, error } = await withCreatedItems('campaign_templates', db().from('campaign_templates')
|
|
247
266
|
.insert({
|
|
248
267
|
project_id: projectId,
|
|
249
|
-
name,
|
|
268
|
+
name: parsedName.data,
|
|
250
269
|
brief_template: brief,
|
|
251
270
|
subject_hint: subject_hint ?? '',
|
|
252
271
|
body_html: body_html ?? null,
|
|
253
272
|
variables: deriveTemplateVariables({ body_html: body_html ?? null, subject_hint: subject_hint ?? '' }),
|
|
254
273
|
category: category ?? 'general',
|
|
255
274
|
description: description ?? '',
|
|
256
|
-
tags: tags ?? [],
|
|
275
|
+
tags: normalizeTagInput(tags ?? []).tags,
|
|
257
276
|
is_transactional: is_transactional ?? false,
|
|
258
277
|
})
|
|
259
278
|
.select('id, name, category, description, subject_hint, brief_template, body_html, body_source, body_text, format, variables, tags, created_at')
|
|
@@ -284,7 +303,7 @@ export function registerTemplateTools(server) {
|
|
|
284
303
|
tags: data.tags,
|
|
285
304
|
createdBy: requestContext()?.actorId ?? null,
|
|
286
305
|
}).catch((err) => console.warn('Failed to snapshot template version for MCP create_template:', err));
|
|
287
|
-
return j({ template: data, ok: true });
|
|
306
|
+
return j({ template: data, ok: true, warnings: templateWarnings(content) });
|
|
288
307
|
});
|
|
289
308
|
server.registerTool('update_template', {
|
|
290
309
|
title: 'Update Template',
|
|
@@ -297,7 +316,7 @@ export function registerTemplateTools(server) {
|
|
|
297
316
|
description: 'Update an existing email template.',
|
|
298
317
|
inputSchema: {
|
|
299
318
|
id: z.string().uuid(),
|
|
300
|
-
name:
|
|
319
|
+
name: templateName.optional(),
|
|
301
320
|
brief: z.string().optional(),
|
|
302
321
|
subject_hint: z.string().optional(),
|
|
303
322
|
body_html: z.string().optional(),
|
|
@@ -313,13 +332,26 @@ export function registerTemplateTools(server) {
|
|
|
313
332
|
.select('id, project_id, body_html, subject_hint')
|
|
314
333
|
.eq('id', id)
|
|
315
334
|
.maybeSingle();
|
|
316
|
-
if (existingErr
|
|
317
|
-
return
|
|
335
|
+
if (existingErr)
|
|
336
|
+
return toolError(dbToolError(existingErr));
|
|
337
|
+
if (!existing)
|
|
338
|
+
return toolError({ error: 'Template not found', code: 'not_found' });
|
|
318
339
|
if (!existing.project_id || existing.project_id !== projectId)
|
|
319
|
-
return
|
|
340
|
+
return toolError({ error: 'Cannot edit system or other-project templates', code: 'forbidden' });
|
|
341
|
+
const content = {
|
|
342
|
+
body_html: updates.body_html !== undefined ? updates.body_html : existing.body_html,
|
|
343
|
+
subject_hint: updates.subject_hint !== undefined ? updates.subject_hint : existing.subject_hint,
|
|
344
|
+
};
|
|
345
|
+
const contentError = templateContentError(content);
|
|
346
|
+
if (contentError)
|
|
347
|
+
return toolError({ error: contentError, code: 'validation_failed' });
|
|
320
348
|
const row = { updated_at: new Date().toISOString() };
|
|
321
|
-
if (updates.name !== undefined)
|
|
322
|
-
|
|
349
|
+
if (updates.name !== undefined) {
|
|
350
|
+
const parsedName = templateName.safeParse(updates.name);
|
|
351
|
+
if (!parsedName.success)
|
|
352
|
+
return toolError({ error: 'Name must contain 1 to 200 characters after trimming', code: 'validation_failed' });
|
|
353
|
+
row.name = parsedName.data;
|
|
354
|
+
}
|
|
323
355
|
if (updates.brief !== undefined)
|
|
324
356
|
row.brief_template = updates.brief;
|
|
325
357
|
if (updates.subject_hint !== undefined)
|
|
@@ -331,13 +363,10 @@ export function registerTemplateTools(server) {
|
|
|
331
363
|
if (updates.description !== undefined)
|
|
332
364
|
row.description = updates.description;
|
|
333
365
|
if (updates.tags !== undefined)
|
|
334
|
-
row.tags = updates.tags;
|
|
366
|
+
row.tags = normalizeTagInput(updates.tags).tags;
|
|
335
367
|
if (updates.is_transactional !== undefined)
|
|
336
368
|
row.is_transactional = updates.is_transactional;
|
|
337
|
-
row.variables = deriveTemplateVariables(
|
|
338
|
-
body_html: updates.body_html !== undefined ? updates.body_html : existing.body_html,
|
|
339
|
-
subject_hint: updates.subject_hint !== undefined ? updates.subject_hint : existing.subject_hint,
|
|
340
|
-
});
|
|
369
|
+
row.variables = deriveTemplateVariables(content);
|
|
341
370
|
const { data, error } = await db().from('campaign_templates')
|
|
342
371
|
.update(row)
|
|
343
372
|
.eq('id', id)
|
|
@@ -370,7 +399,7 @@ export function registerTemplateTools(server) {
|
|
|
370
399
|
tags: data.tags,
|
|
371
400
|
createdBy: requestContext()?.actorId ?? null,
|
|
372
401
|
}).catch((err) => console.warn('Failed to snapshot template version for MCP update_template:', err));
|
|
373
|
-
return j({ template: data, ok: true });
|
|
402
|
+
return j({ template: data, ok: true, warnings: templateWarnings(content) });
|
|
374
403
|
});
|
|
375
404
|
server.registerTool('delete_template', {
|
|
376
405
|
title: 'Delete Template',
|
|
@@ -388,17 +417,19 @@ export function registerTemplateTools(server) {
|
|
|
388
417
|
.select('id, project_id')
|
|
389
418
|
.eq('id', id)
|
|
390
419
|
.maybeSingle();
|
|
391
|
-
if (existingErr
|
|
392
|
-
return
|
|
420
|
+
if (existingErr)
|
|
421
|
+
return toolError(dbToolError(existingErr));
|
|
422
|
+
if (!existing)
|
|
423
|
+
return toolError({ error: 'Template not found', code: 'not_found' });
|
|
393
424
|
if (!existing.project_id || existing.project_id !== projectId)
|
|
394
|
-
return
|
|
425
|
+
return toolError({ error: 'Cannot archive system or other-project templates', code: 'forbidden' });
|
|
395
426
|
const { error } = await db().from('campaign_templates')
|
|
396
427
|
.update({ is_archived: true, updated_at: new Date().toISOString() })
|
|
397
428
|
.eq('id', id)
|
|
398
429
|
.eq('project_id', projectId);
|
|
399
430
|
if (error)
|
|
400
431
|
return j(dbToolError(error));
|
|
401
|
-
return j({ ok: true, deleted: id });
|
|
432
|
+
return j({ ok: true, deleted: id, archived: true });
|
|
402
433
|
}));
|
|
403
434
|
server.registerTool('restore_template_version', {
|
|
404
435
|
title: 'Restore Template Version',
|
|
@@ -419,17 +450,21 @@ export function registerTemplateTools(server) {
|
|
|
419
450
|
.select('id, project_id, body_html, body_text, subject_hint')
|
|
420
451
|
.eq('id', id)
|
|
421
452
|
.maybeSingle();
|
|
422
|
-
if (existingErr
|
|
423
|
-
return
|
|
453
|
+
if (existingErr)
|
|
454
|
+
return toolError(dbToolError(existingErr));
|
|
455
|
+
if (!existing)
|
|
456
|
+
return toolError({ error: 'Template not found', code: 'not_found' });
|
|
424
457
|
if (!existing.project_id || existing.project_id !== projectId)
|
|
425
|
-
return
|
|
458
|
+
return toolError({ error: 'Cannot restore system or other-project templates', code: 'forbidden' });
|
|
426
459
|
const { data: version, error: versionErr } = await db().from('template_versions')
|
|
427
460
|
.select('id, version, name, category, description, subject_hint, brief_template, tags, body_html, body_source, body_text, format')
|
|
428
461
|
.eq('id', version_id)
|
|
429
462
|
.eq('template_id', id)
|
|
430
463
|
.maybeSingle();
|
|
431
|
-
if (versionErr
|
|
432
|
-
return
|
|
464
|
+
if (versionErr)
|
|
465
|
+
return toolError(dbToolError(versionErr));
|
|
466
|
+
if (!version)
|
|
467
|
+
return toolError({ error: 'Version not found', code: 'not_found' });
|
|
433
468
|
const bodyRestored = versionHasBodySnapshot(version);
|
|
434
469
|
const row = {
|
|
435
470
|
updated_at: new Date().toISOString(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.178",
|
|
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",
|