@sendinel/mcp-server 1.0.10 → 1.0.12
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 +35 -22
- package/build/anthropic-model.d.ts +30 -0
- package/build/anthropic-model.js +26 -1
- package/build/db.d.ts +17 -0
- package/build/db.js +15 -1
- package/build/index.js +6 -0
- package/build/lib/action-approvals.js +28 -0
- package/build/lib/brand-asset-persist.d.ts +1 -0
- package/build/lib/brand-asset-persist.js +20 -1
- package/build/lib/composer-blocks.d.ts +25 -25
- package/build/lib/composer-blocks.js +25 -5
- package/build/lib/connector-table-discovery.d.ts +12 -0
- package/build/lib/connector-table-discovery.js +117 -0
- package/build/lib/custom-properties-diff.d.ts +16 -0
- package/build/lib/custom-properties-diff.js +22 -0
- package/build/lib/schedule.d.ts +7 -2
- package/build/lib/schedule.js +8 -0
- package/build/lib/v1-client.d.ts +3 -0
- package/build/lib/v1-client.js +45 -0
- package/build/lib/voice.d.ts +7 -30
- package/build/lib/voice.js +6 -138
- package/build/prompts.d.ts +6 -0
- package/build/prompts.js +18 -0
- package/build/scoped-token.d.ts +1 -1
- package/build/server.d.ts +1 -0
- package/build/server.js +65 -6
- package/build/tools/advisor.js +3 -3
- package/build/tools/analytics.d.ts +1 -0
- package/build/tools/analytics.js +105 -8
- package/build/tools/brand-kit.js +29 -30
- package/build/tools/campaigns.js +349 -60
- package/build/tools/cloud-import.d.ts +2 -0
- package/build/tools/cloud-import.js +76 -0
- package/build/tools/compound.d.ts +11 -0
- package/build/tools/compound.js +37 -7
- package/build/tools/connector.d.ts +2 -0
- package/build/tools/connector.js +280 -0
- package/build/tools/contacts.js +744 -16
- package/build/tools/content.js +228 -0
- package/build/tools/data-proposals.js +1 -1
- package/build/tools/data.js +448 -4
- package/build/tools/delivery-ops.js +525 -7
- package/build/tools/forms.js +7 -6
- package/build/tools/gateway.d.ts +6 -0
- package/build/tools/gateway.js +92 -0
- package/build/tools/gdpr.js +185 -0
- package/build/tools/landing-pages.d.ts +2 -0
- package/build/tools/landing-pages.js +202 -0
- package/build/tools/lists.d.ts +2 -0
- package/build/tools/lists.js +428 -0
- package/build/tools/newsletters.js +1 -8
- package/build/tools/org.js +565 -35
- package/build/tools/segments.js +35 -184
- package/build/tools/signatures.d.ts +2 -0
- package/build/tools/signatures.js +168 -0
- package/build/tools/sites.js +3 -1
- package/build/tools/skills.d.ts +2 -0
- package/build/tools/skills.js +29 -0
- package/build/tools/templates.js +133 -7
- package/package.json +13 -12
package/build/tools/content.js
CHANGED
|
@@ -2,6 +2,88 @@ import { z } from 'zod';
|
|
|
2
2
|
import { adminDb, db } from '../db.js';
|
|
3
3
|
import { getProjectId } from '../project.js';
|
|
4
4
|
const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
5
|
+
const BLOG_IMAGE_FORMATS = ['square_image', 'story_image', 'animated_gif', 'short_video', 'carousel'];
|
|
6
|
+
const BLOG_IMAGE_GOALS = ['generate_leads', 'educate', 'launch_offer', 'build_trust'];
|
|
7
|
+
const BLOG_IMAGE_PLATFORMS = ['linkedin', 'x', 'instagram', 'tiktok', 'youtube_shorts'];
|
|
8
|
+
const BLOG_IMAGE_COMPUTE_PATHS = ['managed', 'byok', 'codex_bridge'];
|
|
9
|
+
const BLOG_IMAGE_SCENES = [
|
|
10
|
+
{ id: 'two-paths', name: 'The Two Paths', narrative_type: 'education', description: 'Fork in the road: old way is overgrown, synchronized path is clear.', formats: ['square_image', 'carousel'], tags: ['framework', 'choice', 'education'] },
|
|
11
|
+
{ id: 'signal-vs-noise', name: 'Signal vs. Noise', narrative_type: 'education', description: 'Character turns a radio dial through static until a clean signal emerges.', formats: ['square_image', 'animated_gif'], tags: ['signal', 'intelligence', 'clarity'] },
|
|
12
|
+
{ id: 'strategy-session', name: 'The Strategy Session', narrative_type: 'education', description: 'Whiteboard covered in connected nodes, arrows, and campaign logic.', formats: ['square_image'], tags: ['strategy', 'planning', 'education'] },
|
|
13
|
+
{ id: 'five-step-framework', name: 'The 5-Step Framework', narrative_type: 'education', description: 'Five-panel deck with one concept per slide.', formats: ['carousel', 'square_image'], tags: ['framework', 'education', 'slides'] },
|
|
14
|
+
{ id: 'comparison-matrix', name: 'The Comparison Matrix', narrative_type: 'education', description: 'Clean table comparing the old approach against the new system.', formats: ['carousel', 'square_image'], tags: ['comparison', 'education', 'sales'] },
|
|
15
|
+
{ id: 'focused-founder', name: 'The Focused Founder', narrative_type: 'identity', description: 'Clean desk, coffee, laptop, and morning focus with warm light.', formats: ['square_image', 'story_image'], tags: ['founder', 'focus', 'lifestyle'] },
|
|
16
|
+
{ id: 'dashboard-reveal', name: 'Dashboard Reveal', narrative_type: 'identity', description: 'Dark screen animates into a clean, organized product dashboard.', formats: ['short_video', 'animated_gif', 'square_image'], tags: ['dashboard', 'product', 'ui'] },
|
|
17
|
+
{ id: 'command-center-boot', name: 'Command Center Boot', narrative_type: 'identity', description: 'System boot sequence reveals an organized founder command center.', formats: ['short_video', 'animated_gif', 'square_image'], tags: ['command-center', 'identity', 'systems'] },
|
|
18
|
+
{ id: 'breakthrough-moment', name: 'The Breakthrough Moment', narrative_type: 'transformation', description: 'Person looks up from the screen with sudden clarity.', formats: ['square_image'], tags: ['clarity', 'breakthrough', 'transformation'] },
|
|
19
|
+
{ id: 'chaos-to-campaign', name: 'Chaos to Campaign', narrative_type: 'transformation', description: 'Messy wall of sticky notes reorganizes into a clean campaign calendar.', formats: ['square_image', 'animated_gif'], tags: ['campaign', 'planning', 'marketing'] },
|
|
20
|
+
{ id: 'before-after-desk', name: 'Before/After Desk', narrative_type: 'transformation', description: 'Split panel shows a chaotic desk beside the same desk with one organized system.', formats: ['square_image', 'carousel'], tags: ['before-after', 'systems', 'clarity'] },
|
|
21
|
+
{ id: 'number-breakdown', name: 'The Number Breakdown', narrative_type: 'proof', description: 'Key metrics become bold visual stats across four slides.', formats: ['carousel', 'square_image'], tags: ['metrics', 'proof', 'analytics'] },
|
|
22
|
+
];
|
|
23
|
+
function clean(value) {
|
|
24
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
25
|
+
}
|
|
26
|
+
function record(value) {
|
|
27
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
28
|
+
}
|
|
29
|
+
function scoreBlogImageScene(scene, text, tags) {
|
|
30
|
+
let score = 0;
|
|
31
|
+
if (/\b(how|guide|framework|teach|explain|tip|educat|learn)\b/i.test(text) && scene.narrative_type === 'education')
|
|
32
|
+
score += 3;
|
|
33
|
+
if (/\b(transform|switch|migrat|before.?after|pivot|fix|solve)\b/i.test(text) && scene.narrative_type === 'transformation')
|
|
34
|
+
score += 3;
|
|
35
|
+
if (/\b(proof|result|metric|case study|testimonial|revenue|customer|win)\b/i.test(text) && scene.narrative_type === 'proof')
|
|
36
|
+
score += 3;
|
|
37
|
+
if (/\b(launch|announc|founder|story|brand|vision|identity)\b/i.test(text) && scene.narrative_type === 'identity')
|
|
38
|
+
score += 2;
|
|
39
|
+
for (const tag of scene.tags) {
|
|
40
|
+
if (tags.includes(tag))
|
|
41
|
+
score += 2;
|
|
42
|
+
else if (text.includes(tag))
|
|
43
|
+
score += 1;
|
|
44
|
+
}
|
|
45
|
+
return score;
|
|
46
|
+
}
|
|
47
|
+
async function resolveSynchronexUserId(projectId) {
|
|
48
|
+
const { data: bundle } = await adminDb()
|
|
49
|
+
.from('synchronex_bundle_projects')
|
|
50
|
+
.select('synchronex_user_id')
|
|
51
|
+
.eq('project_id', projectId)
|
|
52
|
+
.eq('status', 'active')
|
|
53
|
+
.maybeSingle();
|
|
54
|
+
if (bundle?.synchronex_user_id)
|
|
55
|
+
return bundle.synchronex_user_id;
|
|
56
|
+
const { data: project } = await adminDb()
|
|
57
|
+
.from('projects')
|
|
58
|
+
.select('account:accounts(synchronex_account_id)')
|
|
59
|
+
.eq('id', projectId)
|
|
60
|
+
.maybeSingle();
|
|
61
|
+
const account = Array.isArray(project?.account) ? project.account[0] : project?.account;
|
|
62
|
+
return typeof account?.synchronex_account_id === 'string' && account.synchronex_account_id ? account.synchronex_account_id : null;
|
|
63
|
+
}
|
|
64
|
+
async function generateSynchronexVisual(synchronexUserId, params) {
|
|
65
|
+
const serviceToken = process.env.SYNCHRONEX_SERVICE_TOKEN ?? '';
|
|
66
|
+
if (!serviceToken)
|
|
67
|
+
throw new Error('SYNCHRONEX_SERVICE_TOKEN is not configured');
|
|
68
|
+
const baseUrl = process.env.SYNCHRONEX_BASE_URL ?? 'https://synchronex.ai';
|
|
69
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, '')}/api/marketing/studio/generate-visual`, {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: {
|
|
72
|
+
'Content-Type': 'application/json',
|
|
73
|
+
Authorization: `Bearer ${serviceToken}`,
|
|
74
|
+
'x-synchronex-user-id': synchronexUserId,
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify(params),
|
|
77
|
+
});
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
const text = await res.text().catch(() => '');
|
|
80
|
+
throw new Error(`Synchronex generate-visual failed (${res.status}): ${text}`);
|
|
81
|
+
}
|
|
82
|
+
const body = await res.json();
|
|
83
|
+
if (!body.asset?.signed_url)
|
|
84
|
+
throw new Error('Synchronex generate-visual returned no asset.');
|
|
85
|
+
return { asset: { signed_url: body.asset.signed_url }, prompt: body.prompt ?? '' };
|
|
86
|
+
}
|
|
5
87
|
export function registerContentTools(server) {
|
|
6
88
|
server.registerTool('create_asset_link', {
|
|
7
89
|
title: 'Create Asset Link',
|
|
@@ -310,6 +392,152 @@ export function registerContentTools(server) {
|
|
|
310
392
|
return j({ error: error.message });
|
|
311
393
|
return j({ blog_posts: data ?? [] });
|
|
312
394
|
});
|
|
395
|
+
server.registerTool('get_blog_post', {
|
|
396
|
+
title: 'Get Blog Post',
|
|
397
|
+
annotations: {
|
|
398
|
+
readOnlyHint: true,
|
|
399
|
+
destructiveHint: false,
|
|
400
|
+
idempotentHint: true,
|
|
401
|
+
openWorldHint: false,
|
|
402
|
+
},
|
|
403
|
+
description: 'Get one ingested blog post for this project, including cover image and publish fields.',
|
|
404
|
+
inputSchema: {
|
|
405
|
+
blog_post_id: z.string().uuid(),
|
|
406
|
+
},
|
|
407
|
+
}, async ({ blog_post_id }) => {
|
|
408
|
+
const { data, error } = await db().from('blog_posts')
|
|
409
|
+
.select('*')
|
|
410
|
+
.eq('project_id', getProjectId())
|
|
411
|
+
.eq('id', blog_post_id)
|
|
412
|
+
.maybeSingle();
|
|
413
|
+
if (error)
|
|
414
|
+
return j({ error: error.message });
|
|
415
|
+
if (!data)
|
|
416
|
+
return j({ error: 'Blog post not found', code: 'NOT_FOUND' });
|
|
417
|
+
return j({ blog_post: data });
|
|
418
|
+
});
|
|
419
|
+
server.registerTool('recommend_blog_post_images', {
|
|
420
|
+
title: 'Recommend Blog Post Images',
|
|
421
|
+
annotations: {
|
|
422
|
+
readOnlyHint: true,
|
|
423
|
+
destructiveHint: false,
|
|
424
|
+
idempotentHint: true,
|
|
425
|
+
openWorldHint: false,
|
|
426
|
+
},
|
|
427
|
+
description: 'Recommend Content Studio scenes for a blog post featured image using the post title, excerpt, and tags.',
|
|
428
|
+
inputSchema: {
|
|
429
|
+
blog_post_id: z.string().uuid(),
|
|
430
|
+
format: z.enum(BLOG_IMAGE_FORMATS).optional(),
|
|
431
|
+
goal: z.enum(BLOG_IMAGE_GOALS).optional().default('educate'),
|
|
432
|
+
audience: z.string().max(240).optional(),
|
|
433
|
+
limit: z.number().int().min(1).max(12).optional().default(6),
|
|
434
|
+
},
|
|
435
|
+
}, async ({ blog_post_id, format, goal = 'educate', audience, limit = 6 }) => {
|
|
436
|
+
const { data: post, error } = await db().from('blog_posts')
|
|
437
|
+
.select('id, title, excerpt, tags')
|
|
438
|
+
.eq('project_id', getProjectId())
|
|
439
|
+
.eq('id', blog_post_id)
|
|
440
|
+
.maybeSingle();
|
|
441
|
+
if (error)
|
|
442
|
+
return j({ error: error.message });
|
|
443
|
+
if (!post)
|
|
444
|
+
return j({ error: 'Blog post not found', code: 'NOT_FOUND' });
|
|
445
|
+
const tags = Array.isArray(post.tags) ? post.tags.map((tag) => String(tag).toLowerCase()) : [];
|
|
446
|
+
const text = `${goal} ${post.title ?? ''} ${post.excerpt ?? ''} ${audience ?? ''}`.toLowerCase();
|
|
447
|
+
const scenes = BLOG_IMAGE_SCENES
|
|
448
|
+
.filter((scene) => (format ? scene.formats.includes(format) : true))
|
|
449
|
+
.map((scene) => ({ ...scene, score: scoreBlogImageScene(scene, text, tags) }))
|
|
450
|
+
.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name))
|
|
451
|
+
.slice(0, limit);
|
|
452
|
+
return j({ scenes, blog_post: { id: post.id, title: post.title, tags: post.tags ?? [] } });
|
|
453
|
+
});
|
|
454
|
+
server.registerTool('generate_blog_post_image', {
|
|
455
|
+
title: 'Generate Blog Post Image',
|
|
456
|
+
annotations: {
|
|
457
|
+
readOnlyHint: false,
|
|
458
|
+
destructiveHint: false,
|
|
459
|
+
idempotentHint: false,
|
|
460
|
+
openWorldHint: true,
|
|
461
|
+
},
|
|
462
|
+
description: 'Generate and persist a featured image for a blog post using the Content Studio image pipeline.',
|
|
463
|
+
inputSchema: {
|
|
464
|
+
blog_post_id: z.string().uuid(),
|
|
465
|
+
scene_id: z.string().min(1),
|
|
466
|
+
format: z.enum(BLOG_IMAGE_FORMATS).optional(),
|
|
467
|
+
topic: z.string().max(500).optional(),
|
|
468
|
+
audience: z.string().max(240).optional(),
|
|
469
|
+
goal: z.enum(BLOG_IMAGE_GOALS).optional().default('educate'),
|
|
470
|
+
platform: z.enum(BLOG_IMAGE_PLATFORMS).optional().default('linkedin'),
|
|
471
|
+
compute_path: z.enum(BLOG_IMAGE_COMPUTE_PATHS).optional().default('managed'),
|
|
472
|
+
},
|
|
473
|
+
}, async ({ blog_post_id, scene_id, format, topic, audience, goal = 'educate', platform = 'linkedin', compute_path = 'managed' }) => {
|
|
474
|
+
const projectId = getProjectId();
|
|
475
|
+
const { data: post, error } = await db().from('blog_posts')
|
|
476
|
+
.select('id, org_id, title, excerpt, tags, cover_image_url')
|
|
477
|
+
.eq('project_id', projectId)
|
|
478
|
+
.eq('id', blog_post_id)
|
|
479
|
+
.maybeSingle();
|
|
480
|
+
if (error)
|
|
481
|
+
return j({ error: error.message });
|
|
482
|
+
if (!post)
|
|
483
|
+
return j({ error: 'Blog post not found', code: 'NOT_FOUND' });
|
|
484
|
+
const scene = BLOG_IMAGE_SCENES.find((s) => s.id === scene_id);
|
|
485
|
+
const chosenFormat = format ?? scene?.formats[0] ?? 'square_image';
|
|
486
|
+
if (scene && !scene.formats.includes(chosenFormat)) {
|
|
487
|
+
return j({ error: `Scene "${scene_id}" does not support format "${chosenFormat}"`, code: 'INVALID_INPUT' });
|
|
488
|
+
}
|
|
489
|
+
const resolvedTopic = clean(topic) || [post.title, post.excerpt].filter(Boolean).join(': ');
|
|
490
|
+
if (!resolvedTopic || resolvedTopic.length < 3) {
|
|
491
|
+
return j({ error: 'Blog post title is required or topic must be >= 3 chars', code: 'INVALID_INPUT' });
|
|
492
|
+
}
|
|
493
|
+
const synchronexUserId = await resolveSynchronexUserId(projectId);
|
|
494
|
+
if (!synchronexUserId) {
|
|
495
|
+
return j({ error: 'This project has no active Synchronex bundle — image generation is a bundled-plan feature.', code: 'NO_SYNCHRONEX_BUNDLE' });
|
|
496
|
+
}
|
|
497
|
+
const { data: project } = await adminDb().from('projects').select('brand_settings').eq('id', projectId).maybeSingle();
|
|
498
|
+
const brandSettings = record(project?.brand_settings);
|
|
499
|
+
const brandContext = {};
|
|
500
|
+
if (typeof brandSettings.logo_url === 'string' && brandSettings.logo_url.trim())
|
|
501
|
+
brandContext.logo = brandSettings.logo_url.trim();
|
|
502
|
+
if (Array.isArray(brandSettings.brand_colors) && brandSettings.brand_colors.length)
|
|
503
|
+
brandContext.colors = brandSettings.brand_colors;
|
|
504
|
+
if (typeof brandSettings.visual_style === 'string' && brandSettings.visual_style.trim())
|
|
505
|
+
brandContext.visual_style = brandSettings.visual_style.trim();
|
|
506
|
+
try {
|
|
507
|
+
const generated = await generateSynchronexVisual(synchronexUserId, {
|
|
508
|
+
scene_id,
|
|
509
|
+
format: chosenFormat,
|
|
510
|
+
topic: resolvedTopic.slice(0, 500),
|
|
511
|
+
audience: (clean(audience) || 'general audience').slice(0, 240),
|
|
512
|
+
goal,
|
|
513
|
+
platform,
|
|
514
|
+
compute_path,
|
|
515
|
+
...(Object.keys(brandContext).length ? { brand_context: brandContext } : {}),
|
|
516
|
+
});
|
|
517
|
+
const { data: updated, error: updateErr } = await db().from('blog_posts')
|
|
518
|
+
.update({ cover_image_url: generated.asset.signed_url, updated_at: new Date().toISOString() })
|
|
519
|
+
.eq('project_id', projectId)
|
|
520
|
+
.eq('id', blog_post_id)
|
|
521
|
+
.select('id, title, cover_image_url, updated_at')
|
|
522
|
+
.maybeSingle();
|
|
523
|
+
if (updateErr)
|
|
524
|
+
return j({ error: updateErr.message, code: updateErr.code });
|
|
525
|
+
await adminDb().from('audit_log').insert({
|
|
526
|
+
project_id: projectId,
|
|
527
|
+
org_id: post.org_id ?? null,
|
|
528
|
+
actor_type: 'mcp',
|
|
529
|
+
action: 'blog_post.image_generated',
|
|
530
|
+
resource_type: 'blog_post',
|
|
531
|
+
resource_id: blog_post_id,
|
|
532
|
+
details: { scene_id, format: chosenFormat },
|
|
533
|
+
channel: 'mcp',
|
|
534
|
+
}).catch(() => { });
|
|
535
|
+
return j({ scene_id, format: chosenFormat, asset: generated.asset, prompt: generated.prompt, blog_post: updated });
|
|
536
|
+
}
|
|
537
|
+
catch (err) {
|
|
538
|
+
return j({ error: err instanceof Error ? err.message : 'Image generation failed', code: 'IMAGE_GENERATION_FAILED' });
|
|
539
|
+
}
|
|
540
|
+
});
|
|
313
541
|
server.registerTool('list_blog_destinations', {
|
|
314
542
|
title: 'List Blog Destinations',
|
|
315
543
|
annotations: {
|
|
@@ -9,7 +9,7 @@ async function engagementSnapshot(projectId) {
|
|
|
9
9
|
const [{ data: logs }, { count: audienceSize }, { data: segments }] = await Promise.all([
|
|
10
10
|
db().from('email_log').select('status, created_at').eq('project_id', projectId).gte('created_at', since).limit(10000),
|
|
11
11
|
db().from('subscribers').select('id', { count: 'exact', head: true }).eq('project_id', projectId).eq('is_test', false).eq('global_unsubscribed', false),
|
|
12
|
-
db().from('segments').select('id, name, subscriber_count, updated_at').eq('project_id', projectId).order('updated_at', { ascending: false }).limit(5),
|
|
12
|
+
db().from('segments').select('id, name, subscriber_count:cached_count, updated_at').eq('project_id', projectId).order('updated_at', { ascending: false }).limit(5),
|
|
13
13
|
]);
|
|
14
14
|
const rows = logs ?? [];
|
|
15
15
|
const sent = rows.filter((row) => ['sent', 'delivered', 'opened', 'clicked', 'bounced', 'complained'].includes(row.status)).length;
|