@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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const HIGH_SCORE_NAMES = new Set([
|
|
2
|
+
'users', 'subscribers', 'contacts', 'profiles', 'customers', 'members', 'leads',
|
|
3
|
+
'audience', 'mailing_list', 'email_list', 'newsletter_subscribers', 'newsletter',
|
|
4
|
+
'people', 'persons',
|
|
5
|
+
]);
|
|
6
|
+
const MED_SCORE_FRAGMENTS = ['user', 'subscriber', 'contact', 'customer', 'member', 'profile', 'lead'];
|
|
7
|
+
const CAMPAIGN_SIGNAL_COLUMNS = new Set([
|
|
8
|
+
'plan', 'plan_name', 'plan_id', 'plan_tier', 'plan_interval', 'subscription_plan',
|
|
9
|
+
'subscription_status', 'subscription_id', 'subscription_tier', 'tier', 'price_tier',
|
|
10
|
+
'subscription_period_end', 'subscription_period_start', 'subscription_trial_end',
|
|
11
|
+
'current_period_end', 'current_period_start', 'billing_period_end', 'billing_period_start',
|
|
12
|
+
'billing_cycle_anchor', 'billing_interval', 'next_billing_date', 'renewal_date',
|
|
13
|
+
'trial_ends_at', 'trial_end', 'trial_period_days', 'is_trialing', 'in_trial',
|
|
14
|
+
'cancel_at', 'canceled_at', 'cancelled_at', 'cancel_at_period_end', 'cancellation_date',
|
|
15
|
+
'cancellation_reason', 'churned_at', 'churn_reason', 'trial_started_at', 'trial_start',
|
|
16
|
+
'onboarded_at', 'onboarding_completed', 'activated_at', 'converted_at', 'upgraded_at',
|
|
17
|
+
'downgraded_at', 'paused_at', 'reactivated_at', 'last_active_at', 'last_seen_at',
|
|
18
|
+
'last_login_at', 'last_activity_at', 'status', 'account_status', 'user_status', 'is_active',
|
|
19
|
+
'email_verified', 'is_verified', 'email_confirmed', 'verified', 'email_opt_in',
|
|
20
|
+
'marketing_consent', 'notifications_enabled', 'email_notifications', 'push_notifications',
|
|
21
|
+
'sms_notifications', 'notify_email', 'notify_sms', 'notify_push', 'in_app_notifications',
|
|
22
|
+
'notify_marketing', 'notify_push_enabled', 'notify_in_app', 'notify_payment_reminders',
|
|
23
|
+
'notify_weekly_digest', 'notify_monthly_reports', 'referral_source', 'referral_code',
|
|
24
|
+
'invite_code', 'signup_source', 'signup_channel', 'traffic_source', 'acquisition_source',
|
|
25
|
+
'utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term', 'utm_channel',
|
|
26
|
+
'first_touch_source', 'last_touch_source', 'device_type', 'platform', 'signup_device',
|
|
27
|
+
'sign_up_device', 'last_device', 'last_sign_in_device', 'last_os', 'last_browser',
|
|
28
|
+
'signup_platform', 'os', 'operating_system', 'browser', 'app_version', 'country',
|
|
29
|
+
'country_code', 'region', 'locale', 'language', 'lang', 'preferred_language',
|
|
30
|
+
'language_preference', 'city', 'timezone', 'state', 'zip_code', 'postal_code', 'role',
|
|
31
|
+
'user_role', 'account_role', 'is_admin', 'industry', 'vertical', 'company', 'company_name',
|
|
32
|
+
'firm_name', 'account_name', 'organization', 'org_name', 'job_title', 'title', 'position',
|
|
33
|
+
'birthday', 'birthdate', 'birth_date', 'mrr', 'arr', 'ltv', 'cltv', 'arpu', 'revenue',
|
|
34
|
+
'monthly_spend', 'annual_spend', 'credits', 'credits_remaining', 'seats', 'team_size',
|
|
35
|
+
'team_members', 'user_metadata.plan', 'user_metadata.subscription_status',
|
|
36
|
+
'user_metadata.trial_ends_at', 'user_metadata.country', 'user_metadata.locale',
|
|
37
|
+
'user_metadata.role', 'user_metadata.company', 'user_metadata.job_title',
|
|
38
|
+
'user_metadata.industry', 'user_metadata.organization', 'user_metadata.city',
|
|
39
|
+
'user_metadata.timezone', 'user_metadata.marketing_consent', 'user_metadata.email_opt_in',
|
|
40
|
+
'user_metadata.notifications_enabled', 'user_metadata.push_notifications',
|
|
41
|
+
'user_metadata.device_type', 'user_metadata.platform', 'user_metadata.signup_device',
|
|
42
|
+
'user_metadata.os', 'user_metadata.app_version',
|
|
43
|
+
]);
|
|
44
|
+
function scoreTable(name, columns) {
|
|
45
|
+
const lower = name.toLowerCase();
|
|
46
|
+
const normalizedColumns = columns.map((column) => column.toLowerCase());
|
|
47
|
+
const hasEmail = normalizedColumns.some((column) => column === 'email' || column.endsWith('.email'));
|
|
48
|
+
let base = 0;
|
|
49
|
+
if (lower === 'users' || lower === 'auth.users')
|
|
50
|
+
base = 100;
|
|
51
|
+
else if (HIGH_SCORE_NAMES.has(lower))
|
|
52
|
+
base = hasEmail ? 90 : 80;
|
|
53
|
+
else if (MED_SCORE_FRAGMENTS.some((fragment) => lower.includes(fragment)))
|
|
54
|
+
base = hasEmail ? 70 : 55;
|
|
55
|
+
else if (hasEmail)
|
|
56
|
+
base = 40;
|
|
57
|
+
const campaignBonus = Math.min(normalizedColumns.filter((column) => CAMPAIGN_SIGNAL_COLUMNS.has(column)).length * 5, 60);
|
|
58
|
+
return base + campaignBonus;
|
|
59
|
+
}
|
|
60
|
+
function parseOpenApiTables(doc) {
|
|
61
|
+
const paths = doc.paths ?? {};
|
|
62
|
+
const definitions = doc.definitions ?? doc.components?.schemas ?? {};
|
|
63
|
+
return Object.keys(paths)
|
|
64
|
+
.filter((path) => /^\/[^/]+$/.test(path))
|
|
65
|
+
.map((path) => {
|
|
66
|
+
const name = decodeURIComponent(path.slice(1));
|
|
67
|
+
const schema = definitions[name] ?? definitions[`public.${name}`] ?? {};
|
|
68
|
+
const columns = Object.keys(schema.properties ?? {});
|
|
69
|
+
return {
|
|
70
|
+
id: `public.${name}`,
|
|
71
|
+
schema: 'public',
|
|
72
|
+
name,
|
|
73
|
+
label: `public.${name}`,
|
|
74
|
+
columns,
|
|
75
|
+
source: 'rest',
|
|
76
|
+
score: scoreTable(name, columns),
|
|
77
|
+
};
|
|
78
|
+
})
|
|
79
|
+
.filter((table) => !table.name.startsWith('pg_'));
|
|
80
|
+
}
|
|
81
|
+
/** Discover tables through the already-resolved per-project Supabase client. */
|
|
82
|
+
export async function listConnectorTablesFromDb(client) {
|
|
83
|
+
const tables = [];
|
|
84
|
+
try {
|
|
85
|
+
const rest = client?.rest;
|
|
86
|
+
if (rest?.url) {
|
|
87
|
+
const headers = new Headers(rest.headers);
|
|
88
|
+
headers.set('Accept', 'application/openapi+json');
|
|
89
|
+
const response = await (rest.fetch ?? fetch)(rest.url, { headers });
|
|
90
|
+
if (response.ok)
|
|
91
|
+
tables.push(...parseOpenApiTables(await response.json()));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Discovery degrades to auth.users when the OpenAPI document is unavailable.
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const { data, error } = await client.auth.admin.listUsers({ page: 1, perPage: 1 });
|
|
99
|
+
if (!error && data) {
|
|
100
|
+
tables.push({
|
|
101
|
+
id: 'auth.users',
|
|
102
|
+
schema: 'auth',
|
|
103
|
+
name: 'users',
|
|
104
|
+
label: 'auth.users',
|
|
105
|
+
columns: ['id', 'email', 'phone', 'created_at', 'updated_at', 'last_sign_in_at', 'user_metadata', 'app_metadata'],
|
|
106
|
+
source: 'auth',
|
|
107
|
+
score: 100,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Some service keys cannot use the Auth Admin API.
|
|
113
|
+
}
|
|
114
|
+
return tables
|
|
115
|
+
.filter((table, index, list) => list.findIndex((candidate) => candidate.id === table.id) === index)
|
|
116
|
+
.sort((a, b) => b.score - a.score || a.label.localeCompare(b.label));
|
|
117
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type CustomPropertyChange = {
|
|
2
|
+
field: string;
|
|
3
|
+
old_value: unknown;
|
|
4
|
+
new_value: unknown;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Diffs an incoming custom_properties patch against the prior stored value.
|
|
8
|
+
* Only keys present in `patch` are considered (matches the merge semantics
|
|
9
|
+
* already used by every custom_properties writer: a key set to `null` means
|
|
10
|
+
* "delete this key"). Comparison is shallow (`!==`) - nested-object values
|
|
11
|
+
* that are re-sent unchanged will show as a false-positive change; that's an
|
|
12
|
+
* accepted tradeoff (over-reporting a no-op as changed is safer than silently
|
|
13
|
+
* missing a real change) since none of the three call sites this feeds
|
|
14
|
+
* currently write nested-object custom_properties values.
|
|
15
|
+
*/
|
|
16
|
+
export declare function diffCustomProperties(before: Record<string, unknown> | null | undefined, patch: Record<string, unknown>): CustomPropertyChange[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diffs an incoming custom_properties patch against the prior stored value.
|
|
3
|
+
* Only keys present in `patch` are considered (matches the merge semantics
|
|
4
|
+
* already used by every custom_properties writer: a key set to `null` means
|
|
5
|
+
* "delete this key"). Comparison is shallow (`!==`) - nested-object values
|
|
6
|
+
* that are re-sent unchanged will show as a false-positive change; that's an
|
|
7
|
+
* accepted tradeoff (over-reporting a no-op as changed is safer than silently
|
|
8
|
+
* missing a real change) since none of the three call sites this feeds
|
|
9
|
+
* currently write nested-object custom_properties values.
|
|
10
|
+
*/
|
|
11
|
+
export function diffCustomProperties(before, patch) {
|
|
12
|
+
const prev = before ?? {};
|
|
13
|
+
const changes = [];
|
|
14
|
+
for (const key of Object.keys(patch)) {
|
|
15
|
+
const oldValue = prev[key] ?? null;
|
|
16
|
+
const newValue = patch[key] ?? null;
|
|
17
|
+
if (oldValue !== newValue) {
|
|
18
|
+
changes.push({ field: key, old_value: oldValue, new_value: newValue });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return changes;
|
|
22
|
+
}
|
package/build/lib/schedule.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const SCHEDULE_WINDOWS: readonly [
|
|
1
|
+
export declare const SCHEDULE_WINDOWS: readonly ['today', 'tomorrow', '7d', '30d'];
|
|
2
2
|
export type ScheduleWindow = (typeof SCHEDULE_WINDOWS)[number];
|
|
3
3
|
export type ScheduleCampaign = {
|
|
4
4
|
id: string;
|
|
@@ -28,9 +28,14 @@ export declare function getScheduleWindowBounds(window?: ScheduleWindow, now?: D
|
|
|
28
28
|
start: Date;
|
|
29
29
|
end: Date;
|
|
30
30
|
} | {
|
|
31
|
-
window: "
|
|
31
|
+
window: "30d" | "7d" | "today";
|
|
32
32
|
start: Date;
|
|
33
33
|
end: Date;
|
|
34
34
|
};
|
|
35
35
|
export declare function isoDate(value: string | Date): string;
|
|
36
|
+
export declare function parseFutureDate(value: string, fieldName?: string): {
|
|
37
|
+
scheduledAt: string;
|
|
38
|
+
} | {
|
|
39
|
+
error: string;
|
|
40
|
+
};
|
|
36
41
|
export declare function groupScheduleByCampaign(rows: ScheduleEnrollmentRow[]): ScheduleCampaignGroup[];
|
package/build/lib/schedule.js
CHANGED
|
@@ -22,6 +22,14 @@ export function getScheduleWindowBounds(window = 'today', now = new Date()) {
|
|
|
22
22
|
export function isoDate(value) {
|
|
23
23
|
return new Date(value).toISOString().slice(0, 10);
|
|
24
24
|
}
|
|
25
|
+
export function parseFutureDate(value, fieldName = 'scheduled_at') {
|
|
26
|
+
const date = new Date(value);
|
|
27
|
+
if (Number.isNaN(date.getTime()))
|
|
28
|
+
return { error: `${fieldName} must be a valid ISO timestamp` };
|
|
29
|
+
if (date.getTime() <= Date.now())
|
|
30
|
+
return { error: `${fieldName} must be in the future` };
|
|
31
|
+
return { scheduledAt: date.toISOString() };
|
|
32
|
+
}
|
|
25
33
|
function getCampaign(row) {
|
|
26
34
|
if (Array.isArray(row.campaigns))
|
|
27
35
|
return row.campaigns[0] ?? null;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { requestContext } from '../db.js';
|
|
2
|
+
import { getProjectId } from '../project.js';
|
|
3
|
+
function v1ApiBase() {
|
|
4
|
+
return (process.env.NEXT_PUBLIC_APP_URL
|
|
5
|
+
?? process.env.SENDINEL_APP_URL
|
|
6
|
+
?? process.env.APP_URL
|
|
7
|
+
?? 'https://sendinel.ai').replace(/\/$/, '');
|
|
8
|
+
}
|
|
9
|
+
export async function callSendinelV1(path, opts = {}) {
|
|
10
|
+
const bridged = requestContext()?.v1Caller;
|
|
11
|
+
if (bridged)
|
|
12
|
+
return bridged(path, opts);
|
|
13
|
+
const apiKey = process.env.SENDINEL_API_KEY;
|
|
14
|
+
if (!apiKey) {
|
|
15
|
+
const viaHttpTransport = process.env.MCP_TRANSPORT === 'http';
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
status: 0,
|
|
19
|
+
error: viaHttpTransport
|
|
20
|
+
? "This tool requires SENDINEL_API_KEY in this self-hosted HTTP MCP server's environment. It cannot be set from a chat session."
|
|
21
|
+
: "This tool requires SENDINEL_API_KEY in your local stdio MCP client's environment. It cannot be set from a chat session.",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const query = opts.query ? `?${new URLSearchParams(opts.query).toString()}` : '';
|
|
25
|
+
const response = await fetch(`${v1ApiBase()}${path}${query}`, {
|
|
26
|
+
method: opts.method ?? 'POST',
|
|
27
|
+
headers: {
|
|
28
|
+
Authorization: `Bearer ${apiKey}`,
|
|
29
|
+
'X-Sendinel-Project-Id': getProjectId(),
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
},
|
|
32
|
+
...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
|
|
33
|
+
});
|
|
34
|
+
const text = await response.text();
|
|
35
|
+
let parsed = text;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(text);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Preserve non-JSON responses as text.
|
|
41
|
+
}
|
|
42
|
+
return response.ok
|
|
43
|
+
? { ok: true, data: parsed, status: response.status }
|
|
44
|
+
: { ok: false, error: parsed || `HTTP ${response.status}`, status: response.status };
|
|
45
|
+
}
|
package/build/lib/voice.d.ts
CHANGED
|
@@ -1,32 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Re-export of `@sendinel/domain` voice builders.
|
|
3
|
+
* Implementation lives in packages/domain/src/voice.ts — do not add logic here.
|
|
4
|
+
*
|
|
5
|
+
* Second argument is org/project Brand Brain (`BrandSettings`). Call sites may
|
|
6
|
+
* still name the local variable `projectSettings`; the shared signature uses `orgSettings`.
|
|
4
7
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
greeting_style?: string;
|
|
8
|
-
sign_off?: string;
|
|
9
|
-
rules?: string[];
|
|
10
|
-
anti_patterns?: string[];
|
|
11
|
-
max_paragraphs?: number;
|
|
12
|
-
}
|
|
13
|
-
export interface BrandSettings {
|
|
14
|
-
mission?: string;
|
|
15
|
-
audience?: string;
|
|
16
|
-
tone_words?: string[];
|
|
17
|
-
forbidden_phrases?: string[];
|
|
18
|
-
differentiators?: string;
|
|
19
|
-
default_signoff_name?: string;
|
|
20
|
-
ps_line?: string;
|
|
21
|
-
}
|
|
22
|
-
export declare function buildVoicePrompt(siteVoice: Record<string, unknown> | null | undefined, projectSettings?: BrandSettings | null): string;
|
|
23
|
-
export declare function buildEmailSystemPrompt(brandContext: string, voicePrompt: string): string;
|
|
24
|
-
export declare const KNOWN_VARIABLES: Set<string>;
|
|
25
|
-
export declare const STOCK_PHRASES: string[];
|
|
26
|
-
export interface ValidationResult {
|
|
27
|
-
valid: boolean;
|
|
28
|
-
errors: string[];
|
|
29
|
-
warnings: string[];
|
|
30
|
-
}
|
|
31
|
-
export declare function validateTemplate(html: string, voiceConfig?: BrandVoiceConfig | null): ValidationResult;
|
|
32
|
-
export {};
|
|
8
|
+
export { buildEmailSystemPrompt, buildVoicePrompt, deriveTemplateVariables, KNOWN_VARIABLES, STOCK_PHRASES, TEMPLATE_VARIABLE_TOKEN_PATTERN, validateTemplate, } from "@sendinel/domain";
|
|
9
|
+
export type { BrandSettings, ValidationResult } from "@sendinel/domain";
|
package/build/lib/voice.js
CHANGED
|
@@ -1,140 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Re-export of `@sendinel/domain` voice builders.
|
|
3
|
+
* Implementation lives in packages/domain/src/voice.ts — do not add logic here.
|
|
4
|
+
*
|
|
5
|
+
* Second argument is org/project Brand Brain (`BrandSettings`). Call sites may
|
|
6
|
+
* still name the local variable `projectSettings`; the shared signature uses `orgSettings`.
|
|
4
7
|
*/
|
|
5
|
-
|
|
6
|
-
tone: "Direct. Warm. Zero corporate. Write like you're sending a quick email to someone you respect but haven't met in person yet.",
|
|
7
|
-
greeting_style: 'Hey {{first_name}},',
|
|
8
|
-
sign_off: "— Kevin",
|
|
9
|
-
rules: [
|
|
10
|
-
"First name always. Never \"Dear Valued User\" or \"Hi there.\"",
|
|
11
|
-
"Short sentences. If a paragraph hits 3 sentences, break it.",
|
|
12
|
-
"One CTA per email. One. Don't make them choose.",
|
|
13
|
-
"Plain text wins. HTML is just for the button.",
|
|
14
|
-
"No exclamation marks unless something genuinely warrants one.",
|
|
15
|
-
'No "just checking in" or "hope this finds you well."',
|
|
16
|
-
"Say what it is, why it matters, what to do. Done.",
|
|
17
|
-
],
|
|
18
|
-
anti_patterns: [
|
|
19
|
-
"emojis in body text",
|
|
20
|
-
"stock phrases",
|
|
21
|
-
"walls of text",
|
|
22
|
-
"multiple asks or CTAs",
|
|
23
|
-
"em dashes (—)",
|
|
24
|
-
],
|
|
25
|
-
max_paragraphs: 5,
|
|
26
|
-
};
|
|
27
|
-
export function buildVoicePrompt(siteVoice, projectSettings) {
|
|
28
|
-
const v = { ...DEFAULT_VOICE };
|
|
29
|
-
if (projectSettings) {
|
|
30
|
-
if (projectSettings.tone_words?.length) {
|
|
31
|
-
v.tone = `${projectSettings.tone_words.join('. ')}. Write like you're emailing someone you respect.`;
|
|
32
|
-
}
|
|
33
|
-
if (projectSettings.default_signoff_name) {
|
|
34
|
-
v.sign_off = `- ${projectSettings.default_signoff_name}`;
|
|
35
|
-
}
|
|
36
|
-
if (projectSettings.forbidden_phrases?.length) {
|
|
37
|
-
v.anti_patterns = [...v.anti_patterns, ...projectSettings.forbidden_phrases];
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
if (siteVoice && Object.keys(siteVoice).length > 0) {
|
|
41
|
-
const sv = siteVoice;
|
|
42
|
-
if (sv.tone)
|
|
43
|
-
v.tone = sv.tone;
|
|
44
|
-
if (sv.greeting_style)
|
|
45
|
-
v.greeting_style = sv.greeting_style;
|
|
46
|
-
if (sv.sign_off)
|
|
47
|
-
v.sign_off = sv.sign_off;
|
|
48
|
-
if (sv.rules?.length)
|
|
49
|
-
v.rules = sv.rules;
|
|
50
|
-
if (sv.anti_patterns?.length)
|
|
51
|
-
v.anti_patterns = sv.anti_patterns;
|
|
52
|
-
if (sv.max_paragraphs)
|
|
53
|
-
v.max_paragraphs = sv.max_paragraphs;
|
|
54
|
-
}
|
|
55
|
-
const lines = [
|
|
56
|
-
"BRAND VOICE RULES (you MUST follow these):",
|
|
57
|
-
`- Tone: ${v.tone}`,
|
|
58
|
-
`- Greeting: "${v.greeting_style}" — never "Dear" or formal salutations`,
|
|
59
|
-
`- Sign off: "${v.sign_off}" (no title, no logo, no social links)`,
|
|
60
|
-
...v.rules.map((r) => `- ${r}`),
|
|
61
|
-
`- Maximum ${v.max_paragraphs} short paragraphs.`,
|
|
62
|
-
`- NEVER use: ${v.anti_patterns.join(", ")}.`,
|
|
63
|
-
];
|
|
64
|
-
return lines.join("\n");
|
|
65
|
-
}
|
|
66
|
-
export function buildEmailSystemPrompt(brandContext, voicePrompt) {
|
|
67
|
-
return `You are an expert email copywriter writing for a specific brand. ${brandContext}.
|
|
68
|
-
|
|
69
|
-
${voicePrompt}
|
|
70
|
-
|
|
71
|
-
Return a JSON object with keys: "subject" (string), "body_html" (string, clean HTML email with inline styles), "body_text" (string, plain text version). Return ONLY valid JSON, no explanation. Use {{first_name}} as the personalization variable for the recipient's name.`;
|
|
72
|
-
}
|
|
73
|
-
export const KNOWN_VARIABLES = new Set([
|
|
74
|
-
"first_name",
|
|
75
|
-
"last_name",
|
|
76
|
-
"email",
|
|
77
|
-
"unsubscribe_url",
|
|
78
|
-
]);
|
|
79
|
-
export const STOCK_PHRASES = [
|
|
80
|
-
"just checking in",
|
|
81
|
-
"hope this finds you well",
|
|
82
|
-
"hope you're doing well",
|
|
83
|
-
"dear valued",
|
|
84
|
-
"dear customer",
|
|
85
|
-
"dear user",
|
|
86
|
-
"hi there",
|
|
87
|
-
"to whom it may concern",
|
|
88
|
-
"we are pleased to",
|
|
89
|
-
"we are excited to",
|
|
90
|
-
"don't hesitate to",
|
|
91
|
-
"please do not hesitate",
|
|
92
|
-
];
|
|
93
|
-
export function validateTemplate(html, voiceConfig) {
|
|
94
|
-
const errors = [];
|
|
95
|
-
const warnings = [];
|
|
96
|
-
const varPattern = /\{\{(\w+)\}\}/g;
|
|
97
|
-
let match;
|
|
98
|
-
while ((match = varPattern.exec(html)) !== null) {
|
|
99
|
-
if (!KNOWN_VARIABLES.has(match[1])) {
|
|
100
|
-
errors.push(`Unknown variable: {{${match[1]}}}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
const unclosed = html.match(/\{\{[^}]*$/gm);
|
|
104
|
-
if (unclosed) {
|
|
105
|
-
errors.push("Unclosed template variable bracket detected");
|
|
106
|
-
}
|
|
107
|
-
const textContent = html.replace(/<[^>]+>/g, " ").toLowerCase();
|
|
108
|
-
const exclamations = (textContent.match(/!/g) || []).length;
|
|
109
|
-
if (exclamations > 1) {
|
|
110
|
-
warnings.push(`${exclamations} exclamation marks found — voice guide limits to max 1`);
|
|
111
|
-
}
|
|
112
|
-
for (const phrase of STOCK_PHRASES) {
|
|
113
|
-
if (textContent.includes(phrase)) {
|
|
114
|
-
warnings.push(`Stock phrase detected: "${phrase}"`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
const hrefPattern = /href=["']([^"'#]+)["']/gi;
|
|
118
|
-
const hrefs = new Set();
|
|
119
|
-
let hrefMatch;
|
|
120
|
-
while ((hrefMatch = hrefPattern.exec(html)) !== null) {
|
|
121
|
-
const url = hrefMatch[1];
|
|
122
|
-
if (!url.startsWith("mailto:") &&
|
|
123
|
-
!url.includes("unsubscribe") &&
|
|
124
|
-
!url.includes("preferences")) {
|
|
125
|
-
hrefs.add(url);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
if (hrefs.size > 2) {
|
|
129
|
-
warnings.push(`${hrefs.size} distinct CTA links found — voice guide recommends one CTA per email`);
|
|
130
|
-
}
|
|
131
|
-
const signOff = voiceConfig?.sign_off || DEFAULT_VOICE.sign_off;
|
|
132
|
-
if (signOff && !html.includes(signOff) && !html.includes(signOff.replace("—", "–"))) {
|
|
133
|
-
warnings.push(`Missing expected sign-off: "${signOff}"`);
|
|
134
|
-
}
|
|
135
|
-
return {
|
|
136
|
-
valid: errors.length === 0,
|
|
137
|
-
errors,
|
|
138
|
-
warnings,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
8
|
+
export { buildEmailSystemPrompt, buildVoicePrompt, deriveTemplateVariables, KNOWN_VARIABLES, STOCK_PHRASES, TEMPLATE_VARIABLE_TOKEN_PATTERN, validateTemplate, } from "@sendinel/domain";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
export interface PromptRegistrationOptions {
|
|
3
|
+
scopes?: string[];
|
|
4
|
+
toolGroups?: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function registerAllPrompts(server: McpServer, options?: PromptRegistrationOptions): void;
|
package/build/prompts.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { OPERATOR_SKILLS } from '@sendinel/domain';
|
|
2
|
+
export function registerAllPrompts(server, options) {
|
|
3
|
+
const scopes = options?.scopes ?? [];
|
|
4
|
+
if (scopes.includes('gateway'))
|
|
5
|
+
return;
|
|
6
|
+
if (options?.toolGroups !== undefined && !options.toolGroups.includes('skills'))
|
|
7
|
+
return;
|
|
8
|
+
for (const skill of OPERATOR_SKILLS) {
|
|
9
|
+
server.registerPrompt(skill.name, {
|
|
10
|
+
title: skill.name,
|
|
11
|
+
description: skill.description,
|
|
12
|
+
}, async () => ({
|
|
13
|
+
messages: [
|
|
14
|
+
{ role: 'user', content: { type: 'text', text: skill.body } },
|
|
15
|
+
],
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
}
|
package/build/scoped-token.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const SCOPED_TOKEN_SCOPES: readonly [
|
|
1
|
+
export declare const SCOPED_TOKEN_SCOPES: readonly ['data', 'service:provision'];
|
|
2
2
|
export type ScopedTokenScope = (typeof SCOPED_TOKEN_SCOPES)[number];
|
|
3
3
|
export type ScopedTokenClaims = {
|
|
4
4
|
userId: string;
|
package/build/server.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare const ALL_TOOL_GROUPS: string[];
|
|
|
8
8
|
export type ToolGroup = keyof typeof GROUP_REGISTRY;
|
|
9
9
|
export type ToolGroupMap = Record<string, string>;
|
|
10
10
|
export declare function getRequiredScope(name: string): string;
|
|
11
|
+
export declare const GATEWAY_TOOL_NAMES: string[];
|
|
11
12
|
/**
|
|
12
13
|
* Collects tool-name → group metadata from the same group registry used by
|
|
13
14
|
* registerAllTools(). Scope filtering and definition scanning are applied
|