@sendinel/mcp-server 1.0.170 → 1.0.171
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/analytics.js +3 -2
- package/build/tools/campaigns.js +11 -1
- 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
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
|
@@ -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({
|
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.171",
|
|
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",
|