@sendinel/mcp-server 1.0.170 → 1.0.172
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/lib/email-generation.d.ts +4 -0
- package/build/lib/email-generation.js +19 -4
- package/build/tools/analytics.js +3 -2
- package/build/tools/campaigns.js +24 -5
- package/build/tools/contacts.js +19 -17
- package/build/tools/drafts.js +22 -7
- package/build/tools/gdpr.js +2 -1
- package/build/tools/lists.js +2 -1
- package/build/tools/templates.js +2 -1
- package/package.json +1 -1
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type Anthropic from '@anthropic-ai/sdk';
|
|
2
2
|
import { type ComposerBlock } from './composer-blocks.js';
|
|
3
|
+
/** Known unsupported targeting must fail before a model can invent a property rule. */
|
|
4
|
+
export declare function unsupportedAudienceReason(description: string): string | null;
|
|
5
|
+
export declare class AudienceNotUnderstoodError extends Error {
|
|
6
|
+
}
|
|
3
7
|
export declare function parseAudienceRules(anthropic: Anthropic, description: string): Promise<Record<string, unknown>>;
|
|
4
8
|
export type GeneratedEmail = {
|
|
5
9
|
subject: string;
|
|
@@ -11,24 +11,39 @@ import { BLOCK_GENERATION_SCHEMA, blocksToHtml, blocksToText, normalizeComposerB
|
|
|
11
11
|
function parseJsonResponse(raw) {
|
|
12
12
|
return JSON.parse(raw.replace(/```json?\n?|```/g, '').trim());
|
|
13
13
|
}
|
|
14
|
+
const EMAIL_DOMAIN_AUDIENCE_MESSAGE = 'Audiences by email address or domain are not supported. Create a segment using a tag or property rule, or pass segment_id for a saved segment.';
|
|
15
|
+
/** Known unsupported targeting must fail before a model can invent a property rule. */
|
|
16
|
+
export function unsupportedAudienceReason(description) {
|
|
17
|
+
return /\be-?mail[\s_-]+domains?\b|\be-?mail(?:\s+addresses?)?\s+(?:ends?\s+with|contains?|matches?|is\b|at\b)|@[a-z0-9](?:[a-z0-9.-]*\.)[a-z]{2,}\b/i.test(description)
|
|
18
|
+
? EMAIL_DOMAIN_AUDIENCE_MESSAGE : null;
|
|
19
|
+
}
|
|
20
|
+
export class AudienceNotUnderstoodError extends Error {
|
|
21
|
+
}
|
|
14
22
|
export async function parseAudienceRules(anthropic, description) {
|
|
23
|
+
const unsupported = unsupportedAudienceReason(description);
|
|
24
|
+
if (unsupported)
|
|
25
|
+
throw new AudienceNotUnderstoodError(unsupported);
|
|
15
26
|
const resp = await anthropic.messages.create({
|
|
16
27
|
model: getAnthropicModelId(),
|
|
17
28
|
max_tokens: AI_MAX_TOKENS,
|
|
18
29
|
...outputConfig("low"),
|
|
19
|
-
system: 'You translate natural language audience descriptions into a JSON filter rules object. Return ONLY valid JSON. Available keys: lifecycle_stage (lead|trial|customer|churned|win-back), payment (object with plan, mrr_cents, ltv_cents, subscription_status, trial_ends_at, churned_at, paid_at), min_score, max_score, source, subscribed_after, subscribed_before, has_opened, has_clicked, tag, custom_property, exclude_suppressed. Example: {"payment":{"plan":"pro","trial_ends_at":{"within_days":7}},"lifecycle_stage":"trial"}. Today is ' + new Date().toISOString().slice(0, 10) + '.',
|
|
30
|
+
system: 'You translate natural language audience descriptions into a JSON filter rules object. Return ONLY valid JSON. Available keys: match_all, lifecycle_stage (lead|trial|customer|churned|win-back), payment (object with plan, mrr_cents, ltv_cents, subscription_status, trial_ends_at, churned_at, paid_at), min_score, max_score, source, subscribed_after, subscribed_before, has_opened, has_clicked, tag, custom_property (a string like "plan=pro"), exclude_suppressed. Everyone, all contacts, all subscribers or all means {"match_all":true}. There is no email address or email-domain rule. Never invent keys or use custom_property for email targeting; return {"error":"' + EMAIL_DOMAIN_AUDIENCE_MESSAGE + '"} instead. For any other unsupported audience return an error object explaining why. Example: {"payment":{"plan":"pro","trial_ends_at":{"within_days":7}},"lifecycle_stage":"trial"}. Today is ' + new Date().toISOString().slice(0, 10) + '.',
|
|
20
31
|
messages: [{ role: 'user', content: description }],
|
|
21
32
|
});
|
|
22
33
|
const text = anthropicCheckedText(resp);
|
|
34
|
+
let rules;
|
|
23
35
|
try {
|
|
24
|
-
|
|
36
|
+
rules = parseJsonResponse(text);
|
|
25
37
|
if (!rules || typeof rules !== 'object' || Array.isArray(rules))
|
|
26
38
|
throw new Error('not an object');
|
|
27
|
-
return rules;
|
|
28
39
|
}
|
|
29
40
|
catch {
|
|
30
|
-
throw new
|
|
41
|
+
throw new AudienceNotUnderstoodError(`Could not understand the audience "${description}". Try a saved segment instead.`);
|
|
31
42
|
}
|
|
43
|
+
if ('error' in rules)
|
|
44
|
+
throw new AudienceNotUnderstoodError(typeof rules.error === 'string'
|
|
45
|
+
? rules.error : 'This audience is not supported. Try a saved segment instead.');
|
|
46
|
+
return rules;
|
|
32
47
|
}
|
|
33
48
|
export async function generateEmailFromBrief(anthropic, projectId, siteId, brief) {
|
|
34
49
|
const [{ data: site, error: siteError }, { data: project, error: projectError }] = await Promise.all([
|
package/build/tools/analytics.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { literalIlikePattern } from '@sendinel/domain';
|
|
1
2
|
import { isSentRow } from "@sendinel/domain";
|
|
2
3
|
import { readPeriodCampaigns, campaignPeriodFields, filterPlanContacts } from "@sendinel/domain";
|
|
3
4
|
import { isTrueConversionEvent, TRUE_CONVERSION_DEFINITION } from "@sendinel/domain";
|
|
@@ -221,9 +222,9 @@ export function registerAnalyticsTools(server) {
|
|
|
221
222
|
.order('created_at', { ascending: false })
|
|
222
223
|
.limit(limit);
|
|
223
224
|
if (event_name)
|
|
224
|
-
query = query.ilike('event_name',
|
|
225
|
+
query = query.ilike('event_name', literalIlikePattern(event_name));
|
|
225
226
|
if (email)
|
|
226
|
-
query = query.ilike('email',
|
|
227
|
+
query = query.ilike('email', literalIlikePattern(email));
|
|
227
228
|
if (source)
|
|
228
229
|
query = query.eq('source', source);
|
|
229
230
|
if (site_id)
|
package/build/tools/campaigns.js
CHANGED
|
@@ -4,7 +4,7 @@ import { resolveCampaignReentry, readCampaignPeriods, campaignPeriodFields } fro
|
|
|
4
4
|
import { CAMPAIGN_BRANCH_CONDITIONS, campaignBranchConditionValue, isContactPropertyConditionList, STEP_PROPERTY_CONDITIONS_ERROR, validateStepPropertyRules } from "@sendinel/domain";
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import Anthropic from '@anthropic-ai/sdk';
|
|
7
|
-
import { stripActiveEmailContent, assertSendAccepted, providerSendError, sendError, SendRefusedError, BLOCKING_CONTENT_ISSUE_KINDS, computeSendContentKey, findContentIssues, sendingHostOf, NAME_FALLBACKS, ONE_SEND_MATCH_STATUSES, ONE_SEND_SENT_STATUSES, ONE_SEND_WINDOW_DAYS, normalizeTriggerConfig, renderTemplateString as renderTemplateVars, resolveBrandTokens, brandWarnings, readAllRows, isSegmentRulesError, scopeSegmentRulesToSite, SegmentRulesError, validateSegmentRules, validateUnsubFooterTemplate, } from '@sendinel/domain';
|
|
7
|
+
import { stripActiveEmailContent, assertSendAccepted, providerSendError, sendError, SendRefusedError, BLOCKING_CONTENT_ISSUE_KINDS, computeSendContentKey, findContentIssues, sendingHostOf, NAME_FALLBACKS, ONE_SEND_MATCH_STATUSES, ONE_SEND_SENT_STATUSES, ONE_SEND_WINDOW_DAYS, normalizeTriggerConfig, renderTemplateString as renderTemplateVars, resolveBrandTokens, brandWarnings, readAllRows, isSegmentRulesError, scopeSegmentRulesToSite, SegmentRulesError, validateSegmentRules, validateUnsubFooterTemplate, SEGMENT_MATCH_ALL, } from '@sendinel/domain';
|
|
8
8
|
// Subpath import, not the barrel above -- reply-actions.ts uses node:crypto,
|
|
9
9
|
// and MCP-server-side that's fine (this never runs in an Edge Function), but
|
|
10
10
|
// keeping it off the shared barrel avoids re-creating the Edge-Function
|
|
@@ -22,7 +22,7 @@ import { fireWebhookEvent } from '../lib/webhook-events.js';
|
|
|
22
22
|
import { approvalNext, approvalExcerpt, assertChannelCanExecute, currentChannelContext, requestApproval, checkApproval, withApproval } from '../lib/action-approvals.js';
|
|
23
23
|
import { sandboxAudience, broadcastApprovalNext } from '../lib/sandbox-audience.js';
|
|
24
24
|
import { resolveSegmentContactIds, resolveSendAudience } from '../lib/send-audience.js';
|
|
25
|
-
import { generateEmailFromBrief, parseAudienceRules } from '../lib/email-generation.js';
|
|
25
|
+
import { AudienceNotUnderstoodError, generateEmailFromBrief, parseAudienceRules, unsupportedAudienceReason } from '../lib/email-generation.js';
|
|
26
26
|
import { getAnthropicModelId } from '../anthropic-model.js';
|
|
27
27
|
import { AI_MAX_TOKENS, outputConfig, anthropicCheckedText } from '@sendinel/domain';
|
|
28
28
|
import { getScheduleWindowBounds, groupScheduleByCampaign, parseFutureDate } from '../lib/schedule.js';
|
|
@@ -813,6 +813,16 @@ export function registerCampaignTools(server) {
|
|
|
813
813
|
window: z.enum(['today', 'tomorrow', '7d', '30d']).optional().default('today'),
|
|
814
814
|
},
|
|
815
815
|
}, async ({ site_id, window }) => {
|
|
816
|
+
const { data: site, error: siteError } = await db()
|
|
817
|
+
.from('sites')
|
|
818
|
+
.select('id')
|
|
819
|
+
.eq('id', site_id)
|
|
820
|
+
.eq('project_id', getProjectId())
|
|
821
|
+
.maybeSingle();
|
|
822
|
+
if (siteError)
|
|
823
|
+
return dbErrorResult(siteError);
|
|
824
|
+
if (!site)
|
|
825
|
+
return toolError({ error: 'Site not found', code: 'not_found' });
|
|
816
826
|
const { start, end } = getScheduleWindowBounds(window ?? 'today');
|
|
817
827
|
const { data, error } = await db()
|
|
818
828
|
.from('campaign_enrollments')
|
|
@@ -824,7 +834,7 @@ export function registerCampaignTools(server) {
|
|
|
824
834
|
.eq('campaigns.site_id', site_id)
|
|
825
835
|
.order('next_send_at', { ascending: true });
|
|
826
836
|
if (error)
|
|
827
|
-
return
|
|
837
|
+
return dbErrorResult(error);
|
|
828
838
|
const campaigns = groupScheduleByCampaign((data ?? []));
|
|
829
839
|
const total_sends_scheduled = campaigns.reduce((sum, campaign) => sum + campaign.total_sends_scheduled, 0);
|
|
830
840
|
return j({
|
|
@@ -1570,8 +1580,12 @@ export function registerCampaignTools(server) {
|
|
|
1570
1580
|
sites: (sites ?? []).map((s) => ({ id: s.id, name: s.name, from_email: s.from_email })),
|
|
1571
1581
|
});
|
|
1572
1582
|
}
|
|
1583
|
+
const everyone = audience !== undefined && ['everyone', 'all contacts', 'all subscribers', 'all'].includes(audience.trim().toLowerCase().replace(/[.!?,;:]+$/, '').trim());
|
|
1584
|
+
const unsupported = audience ? unsupportedAudienceReason(audience) : null;
|
|
1585
|
+
if (unsupported)
|
|
1586
|
+
return j({ error: unsupported, code: 'AUDIENCE_NOT_UNDERSTOOD' });
|
|
1573
1587
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
1574
|
-
if ((audience || brief) && !apiKey)
|
|
1588
|
+
if (((audience && !everyone) || brief) && !apiKey)
|
|
1575
1589
|
return j({ error: 'ANTHROPIC_API_KEY not configured', code: 'AI_NOT_CONFIGURED' });
|
|
1576
1590
|
const anthropic = apiKey ? new Anthropic({ apiKey }) : null;
|
|
1577
1591
|
let rules;
|
|
@@ -1590,12 +1604,17 @@ export function registerCampaignTools(server) {
|
|
|
1590
1604
|
rules = scopeSegmentRulesToSite(segment.rules, segment.site_id);
|
|
1591
1605
|
segmentName = segment.name;
|
|
1592
1606
|
}
|
|
1607
|
+
else if (everyone) {
|
|
1608
|
+
rules = { ...SEGMENT_MATCH_ALL };
|
|
1609
|
+
segmentName = audience;
|
|
1610
|
+
}
|
|
1593
1611
|
else {
|
|
1594
1612
|
try {
|
|
1595
1613
|
rules = await parseAudienceRules(anthropic, audience);
|
|
1596
1614
|
}
|
|
1597
1615
|
catch (error) {
|
|
1598
|
-
return j({ error: error instanceof
|
|
1616
|
+
return j({ error: error instanceof AudienceNotUnderstoodError ? error.message
|
|
1617
|
+
: error instanceof Error ? dbToolError(error).error : String(error), code: 'AUDIENCE_NOT_UNDERSTOOD' });
|
|
1599
1618
|
}
|
|
1600
1619
|
segmentName = audience;
|
|
1601
1620
|
}
|
package/build/tools/contacts.js
CHANGED
|
@@ -2,7 +2,7 @@ import { normalizeTagInput, tagInputWarnings } from '../lib/tags.js';
|
|
|
2
2
|
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { CONTACT_ENRICHMENT_PROVIDERS } from '@desler/models';
|
|
4
4
|
import { createContactEnrichmentAdapters, enrichContactFromConnections, } from '@desler/models/contact-enrichment';
|
|
5
|
-
import { buildPreviewAttentionEnvelope, applySegmentRules, isSegmentRulesError, ContactPropertyAnomalyLogger, normalizeContactProperties, validateContactPropertyPatch, getAttentionDecision, getContactNeighborhood, getScheduledTouches, findContactDuplicateGroups, readAllPages, readAllRows, chunkIds, } from '@sendinel/domain';
|
|
5
|
+
import { buildPreviewAttentionEnvelope, buildSafeIlikeOrFilter, literalIlikePattern, applySegmentRules, isSegmentRulesError, ContactPropertyAnomalyLogger, normalizeContactProperties, validateContactPropertyPatch, getAttentionDecision, getContactNeighborhood, getScheduledTouches, findContactDuplicateGroups, readAllPages, readAllRows, chunkIds, } from '@sendinel/domain';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { callSendinelV1 } from '../lib/v1-client.js';
|
|
8
8
|
import { db, projectClientForWriteback } from '../db.js';
|
|
@@ -988,9 +988,9 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
988
988
|
.eq('project_id', projectId)
|
|
989
989
|
.maybeSingle();
|
|
990
990
|
if (viewError)
|
|
991
|
-
return
|
|
991
|
+
return dbErrorResult(viewError);
|
|
992
992
|
if (!view)
|
|
993
|
-
return
|
|
993
|
+
return toolError({ error: 'View not found', code: 'not_found' });
|
|
994
994
|
viewFilters = normalizeSubscriberViewFilters(view.filters);
|
|
995
995
|
}
|
|
996
996
|
const viewSearch = viewFilters ? subscriberViewFilterString(viewFilters, 'search') : '';
|
|
@@ -1008,7 +1008,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1008
1008
|
if (viewSiteParam && viewSiteParam !== 'all') {
|
|
1009
1009
|
const resolved = await resolveSubscriberViewSiteId(projectId, viewSiteParam);
|
|
1010
1010
|
if (resolved.error)
|
|
1011
|
-
return
|
|
1011
|
+
return toolError(resolved);
|
|
1012
1012
|
viewSiteId = resolved.siteId;
|
|
1013
1013
|
}
|
|
1014
1014
|
const embedOk = await hasCompaniesEmbed(projectId);
|
|
@@ -1021,16 +1021,18 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1021
1021
|
.limit(limit ?? 50);
|
|
1022
1022
|
if (!include_unsubscribed)
|
|
1023
1023
|
q = q.eq('global_unsubscribed', false);
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
if (
|
|
1027
|
-
q = q.or(
|
|
1024
|
+
const searchFilter = buildSafeIlikeOrFilter(['email', 'first_name', 'last_name'], search);
|
|
1025
|
+
const viewSearchFilter = buildSafeIlikeOrFilter(['email', 'first_name', 'last_name'], viewSearch);
|
|
1026
|
+
if (searchFilter)
|
|
1027
|
+
q = q.or(searchFilter);
|
|
1028
|
+
if (viewSearchFilter)
|
|
1029
|
+
q = q.or(viewSearchFilter);
|
|
1028
1030
|
if (viewTags.length > 0)
|
|
1029
1031
|
q = q.contains('tags', viewTags);
|
|
1030
1032
|
if (viewStage)
|
|
1031
1033
|
q = q.eq('lifecycle_stage', viewStage);
|
|
1032
1034
|
if (viewCustomPropertyKey && viewCustomPropertyValue && SUBSCRIBER_VIEW_CUSTOM_PROPERTY_KEY_RE.test(viewCustomPropertyKey)) {
|
|
1033
|
-
q = q.filter(subscriberViewCustomPropertyPath(viewCustomPropertyKey), 'ilike',
|
|
1035
|
+
q = q.filter(subscriberViewCustomPropertyPath(viewCustomPropertyKey), 'ilike', literalIlikePattern(viewCustomPropertyValue));
|
|
1034
1036
|
}
|
|
1035
1037
|
if (viewFilters && viewFilters.show_test !== true)
|
|
1036
1038
|
q = q.not('is_test', 'is', true);
|
|
@@ -1054,7 +1056,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1054
1056
|
.select('contact_id, subscribed')
|
|
1055
1057
|
.eq('site_id', statusSiteId);
|
|
1056
1058
|
if (siteSubscriptionsError)
|
|
1057
|
-
return
|
|
1059
|
+
return dbErrorResult(siteSubscriptionsError);
|
|
1058
1060
|
if (viewStatus === 'not_subscribed') {
|
|
1059
1061
|
const subscribedIds = (siteSubscriptions ?? [])
|
|
1060
1062
|
.map((subscription) => subscription.contact_id)
|
|
@@ -1086,7 +1088,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1086
1088
|
}
|
|
1087
1089
|
const { data, error } = await q;
|
|
1088
1090
|
if (error)
|
|
1089
|
-
return
|
|
1091
|
+
return dbErrorResult(error);
|
|
1090
1092
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
1091
1093
|
});
|
|
1092
1094
|
server.registerTool('list_subscriber_views', {
|
|
@@ -1250,12 +1252,12 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1250
1252
|
.eq('project_id', projectId)
|
|
1251
1253
|
.order('name', { ascending: true })
|
|
1252
1254
|
.limit(limit ?? 50);
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1255
|
+
const searchFilter = buildSafeIlikeOrFilter(['name', 'domain'], search);
|
|
1256
|
+
if (searchFilter)
|
|
1257
|
+
query = query.or(searchFilter);
|
|
1256
1258
|
const { data: companies, error } = await query;
|
|
1257
1259
|
if (error)
|
|
1258
|
-
return
|
|
1260
|
+
return dbErrorResult(error);
|
|
1259
1261
|
const rows = companies ?? [];
|
|
1260
1262
|
if (!rows.length)
|
|
1261
1263
|
return j([]);
|
|
@@ -1270,7 +1272,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1270
1272
|
.order('id')
|
|
1271
1273
|
.range(offset, offset + 999);
|
|
1272
1274
|
if (subscriberError)
|
|
1273
|
-
return
|
|
1275
|
+
return dbErrorResult(subscriberError);
|
|
1274
1276
|
for (const subscriber of subscribers ?? []) {
|
|
1275
1277
|
const companyId = typeof subscriber.company_id === 'string' ? subscriber.company_id : null;
|
|
1276
1278
|
if (!companyId)
|
|
@@ -1638,7 +1640,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1638
1640
|
if (before)
|
|
1639
1641
|
q = q.lte('sent_at', before);
|
|
1640
1642
|
if (subject_contains)
|
|
1641
|
-
q = q.ilike('subject',
|
|
1643
|
+
q = q.ilike('subject', literalIlikePattern(subject_contains));
|
|
1642
1644
|
const { data, error } = await q;
|
|
1643
1645
|
if (error)
|
|
1644
1646
|
return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
|
package/build/tools/drafts.js
CHANGED
|
@@ -3,7 +3,22 @@ import { db } from '../db.js';
|
|
|
3
3
|
import { getProjectId } from '../project.js';
|
|
4
4
|
import { checkPlanLimit } from '../plan-limits.js';
|
|
5
5
|
import { recordBrandAssetUsage } from '../lib/brand-asset-usage.js';
|
|
6
|
-
import { dbToolError } from '../lib/db-error.js';
|
|
6
|
+
import { dbErrorResult, dbToolError, toolError } from '../lib/db-error.js';
|
|
7
|
+
// Called only after the conditional write matched no pending draft. Keep the
|
|
8
|
+
// status predicate on the write so concurrent decisions cannot overwrite one another.
|
|
9
|
+
async function draftDecisionMiss(draftId) {
|
|
10
|
+
const { data, error } = await db()
|
|
11
|
+
.from('ai_drafts')
|
|
12
|
+
.select('id')
|
|
13
|
+
.eq('id', draftId)
|
|
14
|
+
.eq('project_id', getProjectId())
|
|
15
|
+
.maybeSingle();
|
|
16
|
+
if (error)
|
|
17
|
+
return dbErrorResult(error);
|
|
18
|
+
return data
|
|
19
|
+
? toolError({ error: 'Draft is no longer pending', code: 'conflict' })
|
|
20
|
+
: toolError({ error: 'Draft not found', code: 'not_found' });
|
|
21
|
+
}
|
|
7
22
|
export function registerDraftTools(server) {
|
|
8
23
|
server.registerTool('create_draft', {
|
|
9
24
|
title: 'Create Draft',
|
|
@@ -230,11 +245,11 @@ export function registerDraftTools(server) {
|
|
|
230
245
|
.eq('project_id', getProjectId())
|
|
231
246
|
.eq('status', 'pending')
|
|
232
247
|
.select()
|
|
233
|
-
.
|
|
248
|
+
.maybeSingle();
|
|
234
249
|
if (error)
|
|
235
|
-
return
|
|
250
|
+
return dbErrorResult(error);
|
|
236
251
|
if (!data)
|
|
237
|
-
return
|
|
252
|
+
return draftDecisionMiss(draft_id);
|
|
238
253
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
239
254
|
});
|
|
240
255
|
server.registerTool('reject_draft', {
|
|
@@ -258,11 +273,11 @@ export function registerDraftTools(server) {
|
|
|
258
273
|
.eq('project_id', getProjectId())
|
|
259
274
|
.eq('status', 'pending')
|
|
260
275
|
.select()
|
|
261
|
-
.
|
|
276
|
+
.maybeSingle();
|
|
262
277
|
if (error)
|
|
263
|
-
return
|
|
278
|
+
return dbErrorResult(error);
|
|
264
279
|
if (!data)
|
|
265
|
-
return
|
|
280
|
+
return draftDecisionMiss(draft_id);
|
|
266
281
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
267
282
|
});
|
|
268
283
|
}
|
package/build/tools/gdpr.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { literalIlikePattern } from '@sendinel/domain';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
import { db } from '../db.js';
|
|
3
4
|
import { getProjectId } from '../project.js';
|
|
@@ -104,7 +105,7 @@ export function registerGdprTools(server) {
|
|
|
104
105
|
if (request_type)
|
|
105
106
|
query = query.eq('request_type', request_type);
|
|
106
107
|
if (search)
|
|
107
|
-
query = query.ilike('email',
|
|
108
|
+
query = query.ilike('email', literalIlikePattern(search));
|
|
108
109
|
const { data, error } = await query
|
|
109
110
|
.order('created_at', { ascending: false })
|
|
110
111
|
.limit(limit ?? 50);
|
package/build/tools/lists.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { literalIlikePattern } from '@sendinel/domain';
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
import { applySegmentRules, buildSegmentBaseQuery, evaluateSegmentRules, hydrateEmailSendsContacts, isSegmentRulesError, normalizeSegmentRulesToTree, readAllRows, } from '@sendinel/domain';
|
|
3
4
|
import { db } from '../db.js';
|
|
@@ -93,7 +94,7 @@ export function registerListTools(server) {
|
|
|
93
94
|
.eq('project_id', projectId);
|
|
94
95
|
const trimmedSearch = search?.trim();
|
|
95
96
|
if (trimmedSearch)
|
|
96
|
-
query = query.ilike('name',
|
|
97
|
+
query = query.ilike('name', literalIlikePattern(trimmedSearch));
|
|
97
98
|
const { data, error } = await query
|
|
98
99
|
.order('created_at', { ascending: false })
|
|
99
100
|
.limit(limit);
|
package/build/tools/templates.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { literalIlikePattern } from '@sendinel/domain';
|
|
1
2
|
import { withApproval } from '../lib/action-approvals.js';
|
|
2
3
|
import { snapshotTemplateVersion, versionHasBodySnapshot } from '@sendinel/domain';
|
|
3
4
|
import { z } from 'zod';
|
|
@@ -89,7 +90,7 @@ export function registerTemplateTools(server) {
|
|
|
89
90
|
if (category)
|
|
90
91
|
query = query.eq('category', category);
|
|
91
92
|
if (search)
|
|
92
|
-
query = query.ilike('name',
|
|
93
|
+
query = query.ilike('name', literalIlikePattern(search));
|
|
93
94
|
const { data, error } = await query;
|
|
94
95
|
if (error)
|
|
95
96
|
return j(dbToolError(error));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.172",
|
|
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",
|