@sendinel/mcp-server 1.0.174 → 1.0.175
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/contacts.js +73 -80
- package/build/tools/segments.js +2 -2
- package/package.json +1 -1
package/build/tools/contacts.js
CHANGED
|
@@ -2337,55 +2337,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2337
2337
|
return toolError({ error: error.message, code: 'validation_failed' });
|
|
2338
2338
|
throw error;
|
|
2339
2339
|
}
|
|
2340
|
-
let
|
|
2341
|
-
.from('subscribers')
|
|
2342
|
-
.select('id, email, first_name, last_name, engagement_score, lifecycle_stage, source, created_at, last_opened_at, last_clicked_at')
|
|
2343
|
-
.eq('project_id', projectId)
|
|
2344
|
-
.eq('global_unsubscribed', false);
|
|
2345
|
-
if (params.lifecycle_stage)
|
|
2346
|
-
q = q.eq('lifecycle_stage', params.lifecycle_stage);
|
|
2347
|
-
if (params.min_score !== undefined)
|
|
2348
|
-
q = q.gte('engagement_score', params.min_score);
|
|
2349
|
-
if (params.max_score !== undefined)
|
|
2350
|
-
q = q.lte('engagement_score', params.max_score);
|
|
2351
|
-
if (params.source)
|
|
2352
|
-
q = q.eq('source', params.source);
|
|
2353
|
-
if (params.subscribed_after)
|
|
2354
|
-
q = q.gte('created_at', `${params.subscribed_after}T00:00:00Z`);
|
|
2355
|
-
if (params.subscribed_before)
|
|
2356
|
-
q = q.lte('created_at', `${params.subscribed_before}T23:59:59Z`);
|
|
2357
|
-
if (params.has_opened === true)
|
|
2358
|
-
q = q.not('last_opened_at', 'is', null);
|
|
2359
|
-
if (params.has_opened === false)
|
|
2360
|
-
q = q.is('last_opened_at', null);
|
|
2361
|
-
if (params.has_clicked === true)
|
|
2362
|
-
q = q.not('last_clicked_at', 'is', null);
|
|
2363
|
-
if (params.has_clicked === false)
|
|
2364
|
-
q = q.is('last_clicked_at', null);
|
|
2365
|
-
if (params.tag)
|
|
2366
|
-
q = q.contains('tags', [params.tag]);
|
|
2367
|
-
if (params.company_id)
|
|
2368
|
-
q = q.eq('company_id', params.company_id);
|
|
2369
|
-
const fieldTypes = params.custom_property ? await loadContactPropertyDefinitions(projectId) : {};
|
|
2370
|
-
if (params.custom_property) {
|
|
2371
|
-
try {
|
|
2372
|
-
// #4270: the query runs as-is, so a filter it cannot express is an error, not dropped.
|
|
2373
|
-
({ query: q } = await applySegmentRules(q, { custom_property: params.custom_property }, db(), projectId, fieldTypes, { pushDownOnly: true }));
|
|
2374
|
-
}
|
|
2375
|
-
catch (error) {
|
|
2376
|
-
if (isSegmentRulesError(error))
|
|
2377
|
-
return toolError({ error: error.message, code: 'validation_failed' });
|
|
2378
|
-
throw error;
|
|
2379
|
-
}
|
|
2380
|
-
}
|
|
2381
|
-
if (params.site_id) {
|
|
2382
|
-
// Shared site filter: an inner join, not an id list that overflows past ~350 contacts (#3674).
|
|
2383
|
-
// Deliberately everyone on the site, so allowEmpty.
|
|
2384
|
-
const siteFiltered = await applySegmentRules(q, { site_id: params.site_id }, db(), projectId, {}, { allowEmpty: true });
|
|
2385
|
-
if (siteFiltered.siteContactIds && siteFiltered.siteContactIds.length === 0)
|
|
2386
|
-
return j({ total: 0, sample: [] });
|
|
2387
|
-
q = siteFiltered.query;
|
|
2388
|
-
}
|
|
2340
|
+
let companyIds;
|
|
2389
2341
|
if (params.company_domain) {
|
|
2390
2342
|
const { data: companies, error } = await db()
|
|
2391
2343
|
.from('companies')
|
|
@@ -2397,45 +2349,86 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2397
2349
|
const ids = (companies ?? []).map((company) => company.id);
|
|
2398
2350
|
if (!ids.length)
|
|
2399
2351
|
return j({ total: 0, sample: [] });
|
|
2400
|
-
|
|
2352
|
+
companyIds = ids;
|
|
2401
2353
|
}
|
|
2354
|
+
const fieldTypes = params.custom_property ? await loadContactPropertyDefinitions(projectId) : {};
|
|
2355
|
+
const applyEstimateFilters = async (baseQuery) => {
|
|
2356
|
+
let filtered = baseQuery;
|
|
2357
|
+
if (params.lifecycle_stage)
|
|
2358
|
+
filtered = filtered.eq('lifecycle_stage', params.lifecycle_stage);
|
|
2359
|
+
if (params.min_score !== undefined)
|
|
2360
|
+
filtered = filtered.gte('engagement_score', params.min_score);
|
|
2361
|
+
if (params.max_score !== undefined)
|
|
2362
|
+
filtered = filtered.lte('engagement_score', params.max_score);
|
|
2363
|
+
if (params.source)
|
|
2364
|
+
filtered = filtered.eq('source', params.source);
|
|
2365
|
+
if (params.subscribed_after)
|
|
2366
|
+
filtered = filtered.gte('created_at', `${params.subscribed_after}T00:00:00Z`);
|
|
2367
|
+
if (params.subscribed_before)
|
|
2368
|
+
filtered = filtered.lte('created_at', `${params.subscribed_before}T23:59:59Z`);
|
|
2369
|
+
if (params.has_opened === true)
|
|
2370
|
+
filtered = filtered.not('last_opened_at', 'is', null);
|
|
2371
|
+
if (params.has_opened === false)
|
|
2372
|
+
filtered = filtered.is('last_opened_at', null);
|
|
2373
|
+
if (params.has_clicked === true)
|
|
2374
|
+
filtered = filtered.not('last_clicked_at', 'is', null);
|
|
2375
|
+
if (params.has_clicked === false)
|
|
2376
|
+
filtered = filtered.is('last_clicked_at', null);
|
|
2377
|
+
if (params.tag)
|
|
2378
|
+
filtered = filtered.contains('tags', [params.tag]);
|
|
2379
|
+
if (params.company_id)
|
|
2380
|
+
filtered = filtered.eq('company_id', params.company_id);
|
|
2381
|
+
if (companyIds)
|
|
2382
|
+
filtered = filtered.in('company_id', companyIds);
|
|
2383
|
+
if (params.custom_property) {
|
|
2384
|
+
try {
|
|
2385
|
+
// #4270: the query runs as-is, so a filter it cannot express is an error, not dropped.
|
|
2386
|
+
const res = await applySegmentRules(filtered, { custom_property: params.custom_property }, db(), projectId, fieldTypes, { pushDownOnly: true });
|
|
2387
|
+
filtered = res.query;
|
|
2388
|
+
}
|
|
2389
|
+
catch (error) {
|
|
2390
|
+
if (isSegmentRulesError(error))
|
|
2391
|
+
return { query: filtered, error: toolError({ error: error.message, code: 'validation_failed' }) };
|
|
2392
|
+
throw error;
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
if (params.site_id) {
|
|
2396
|
+
// Shared site filter: an inner join, not an id list that overflows past ~350 contacts (#3674).
|
|
2397
|
+
// Deliberately everyone on the site, so allowEmpty.
|
|
2398
|
+
const siteFiltered = await applySegmentRules(filtered, { site_id: params.site_id }, db(), projectId, {}, { allowEmpty: true });
|
|
2399
|
+
if (siteFiltered.siteContactIds && siteFiltered.siteContactIds.length === 0) {
|
|
2400
|
+
return { query: siteFiltered.query, empty: true };
|
|
2401
|
+
}
|
|
2402
|
+
filtered = siteFiltered.query;
|
|
2403
|
+
}
|
|
2404
|
+
return { query: filtered };
|
|
2405
|
+
};
|
|
2406
|
+
let sampleBase = db()
|
|
2407
|
+
.from('subscribers')
|
|
2408
|
+
.select('id, email, first_name, last_name, engagement_score, lifecycle_stage, source, created_at, last_opened_at, last_clicked_at')
|
|
2409
|
+
.eq('project_id', projectId)
|
|
2410
|
+
.eq('global_unsubscribed', false);
|
|
2411
|
+
const sampleFilterResult = await applyEstimateFilters(sampleBase);
|
|
2412
|
+
if (sampleFilterResult.error)
|
|
2413
|
+
return sampleFilterResult.error;
|
|
2414
|
+
if (sampleFilterResult.empty)
|
|
2415
|
+
return j({ total: 0, sample: [] });
|
|
2416
|
+
let q = sampleFilterResult.query;
|
|
2402
2417
|
const { data, error } = await q.order('engagement_score', { ascending: false }).limit(sampleSize);
|
|
2403
2418
|
if (error)
|
|
2404
2419
|
return dbErrorResult(error);
|
|
2405
|
-
// Get full count separately
|
|
2406
|
-
let
|
|
2420
|
+
// Get full count separately using the exact same filters
|
|
2421
|
+
let countBase = db()
|
|
2407
2422
|
.from('subscribers')
|
|
2408
2423
|
.select('id', { count: 'exact', head: true })
|
|
2409
2424
|
.eq('project_id', projectId)
|
|
2410
2425
|
.eq('global_unsubscribed', false);
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
if (params.source)
|
|
2418
|
-
countQ = countQ.eq('source', params.source);
|
|
2419
|
-
if (params.subscribed_after)
|
|
2420
|
-
countQ = countQ.gte('created_at', `${params.subscribed_after}T00:00:00Z`);
|
|
2421
|
-
if (params.subscribed_before)
|
|
2422
|
-
countQ = countQ.lte('created_at', `${params.subscribed_before}T23:59:59Z`);
|
|
2423
|
-
if (params.has_opened === true)
|
|
2424
|
-
countQ = countQ.not('last_opened_at', 'is', null);
|
|
2425
|
-
if (params.has_opened === false)
|
|
2426
|
-
countQ = countQ.is('last_opened_at', null);
|
|
2427
|
-
if (params.has_clicked === true)
|
|
2428
|
-
countQ = countQ.not('last_clicked_at', 'is', null);
|
|
2429
|
-
if (params.has_clicked === false)
|
|
2430
|
-
countQ = countQ.is('last_clicked_at', null);
|
|
2431
|
-
if (params.tag)
|
|
2432
|
-
countQ = countQ.contains('tags', [params.tag]);
|
|
2433
|
-
if (params.custom_property) {
|
|
2434
|
-
({ query: countQ } = await applySegmentRules(countQ, { custom_property: params.custom_property }, db(), projectId, fieldTypes, { pushDownOnly: true }));
|
|
2435
|
-
}
|
|
2436
|
-
// The total must count the same site the sample came from (#3674).
|
|
2437
|
-
if (params.site_id)
|
|
2438
|
-
({ query: countQ } = await applySegmentRules(countQ, { site_id: params.site_id }, db(), projectId, {}, { allowEmpty: true }));
|
|
2426
|
+
const countFilterResult = await applyEstimateFilters(countBase);
|
|
2427
|
+
if (countFilterResult.error)
|
|
2428
|
+
return countFilterResult.error;
|
|
2429
|
+
if (countFilterResult.empty)
|
|
2430
|
+
return j({ total: 0, sample: [] });
|
|
2431
|
+
let countQ = countFilterResult.query;
|
|
2439
2432
|
const { count, error: countError } = await countQ;
|
|
2440
2433
|
if (countError)
|
|
2441
2434
|
return dbErrorResult(countError);
|
package/build/tools/segments.js
CHANGED
|
@@ -248,14 +248,14 @@ export function registerSegmentTools(server) {
|
|
|
248
248
|
if (error)
|
|
249
249
|
return dbErrorResult(error);
|
|
250
250
|
if (!seg)
|
|
251
|
-
return
|
|
251
|
+
return toolError({ error: 'Segment not found', code: 'not_found' });
|
|
252
252
|
segRules = seg.rules;
|
|
253
253
|
}
|
|
254
254
|
else if (rules !== undefined) {
|
|
255
255
|
segRules = rules;
|
|
256
256
|
}
|
|
257
257
|
else {
|
|
258
|
-
return
|
|
258
|
+
return toolError({ error: 'Provide either segment_id or rules.', code: 'validation_failed' });
|
|
259
259
|
}
|
|
260
260
|
let ids;
|
|
261
261
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.175",
|
|
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",
|