@sendinel/mcp-server 1.0.28 → 1.0.30
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.
|
@@ -155,6 +155,10 @@ export function buildApprovalSummary(action, params) {
|
|
|
155
155
|
const fn = summaries[action];
|
|
156
156
|
return fn ? fn(params) : `Execute ${action} with provided parameters`;
|
|
157
157
|
}
|
|
158
|
+
function approvalPageUrl(approvalId) {
|
|
159
|
+
const base = (process.env.NEXT_PUBLIC_APP_URL ?? process.env.SENDINEL_APP_URL ?? process.env.APP_URL ?? 'https://sendinel.ai').replace(/\/$/, '');
|
|
160
|
+
return `${base}/dashboard/approvals/${approvalId}`;
|
|
161
|
+
}
|
|
158
162
|
function normalizeApprovalParams(value) {
|
|
159
163
|
if (Array.isArray(value)) {
|
|
160
164
|
return value.map(normalizeApprovalParams);
|
|
@@ -209,7 +213,8 @@ export async function requestApproval(action, params, apiKeyId, channelContext)
|
|
|
209
213
|
action,
|
|
210
214
|
summary,
|
|
211
215
|
expires_at: expiresAt,
|
|
212
|
-
approve_via: 'Dashboard → Pending Approvals, or Slack /sendinel approve',
|
|
216
|
+
approve_via: 'Open approve_url (works on a phone), Dashboard → Pending Approvals, or Slack /sendinel approve',
|
|
217
|
+
approve_url: approvalPageUrl(data.id),
|
|
213
218
|
message: `This action requires human approval. A notification has been sent to your dashboard. Approval ID: ${data.id}. Check status with: poll this approval_id, or wait for the webhook callback (event: approval.decided). Token expires in 30 minutes.`,
|
|
214
219
|
};
|
|
215
220
|
}
|
package/build/tools/contacts.js
CHANGED
|
@@ -1921,15 +1921,11 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1921
1921
|
({ query: q } = await applySegmentRules(q, { custom_property: params.custom_property }, db(), projectId, fieldTypes));
|
|
1922
1922
|
}
|
|
1923
1923
|
if (params.site_id) {
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
.eq('site_id', params.site_id)
|
|
1928
|
-
.eq('subscribed', true);
|
|
1929
|
-
const ids = (subs ?? []).map((s) => s.contact_id);
|
|
1930
|
-
if (!ids.length)
|
|
1924
|
+
// Shared site filter: an inner join, not an id list that overflows past ~350 contacts (#3674).
|
|
1925
|
+
const siteFiltered = await applySegmentRules(q, { site_id: params.site_id }, db(), projectId);
|
|
1926
|
+
if (siteFiltered.siteContactIds && siteFiltered.siteContactIds.length === 0)
|
|
1931
1927
|
return j({ total: 0, sample: [] });
|
|
1932
|
-
q =
|
|
1928
|
+
q = siteFiltered.query;
|
|
1933
1929
|
}
|
|
1934
1930
|
if (params.company_domain) {
|
|
1935
1931
|
const { data: companies } = await db()
|
|
@@ -1976,6 +1972,9 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1976
1972
|
if (params.custom_property) {
|
|
1977
1973
|
({ query: countQ } = await applySegmentRules(countQ, { custom_property: params.custom_property }, db(), projectId, fieldTypes));
|
|
1978
1974
|
}
|
|
1975
|
+
// The total must count the same site the sample came from (#3674).
|
|
1976
|
+
if (params.site_id)
|
|
1977
|
+
({ query: countQ } = await applySegmentRules(countQ, { site_id: params.site_id }, db(), projectId));
|
|
1979
1978
|
const { count } = await countQ;
|
|
1980
1979
|
return j({ total: count ?? (data ?? []).length, sample: data ?? [] });
|
|
1981
1980
|
});
|
package/build/tools/lists.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { applySegmentRules, buildSegmentBaseQuery, evaluateSegmentRules, normalizeSegmentRulesToTree, } from '@sendinel/domain';
|
|
2
|
+
import { applySegmentRules, buildSegmentBaseQuery, evaluateSegmentRules, normalizeSegmentRulesToTree, readAllRows, } from '@sendinel/domain';
|
|
3
3
|
import { db } from '../db.js';
|
|
4
4
|
import { getProjectId } from '../project.js';
|
|
5
5
|
import { logAudit } from '../audit.js';
|
|
@@ -34,17 +34,13 @@ async function getSegmentContactIds(rules) {
|
|
|
34
34
|
const { query: filtered, siteContactIds } = await applySegmentRules(q, rules, db(), projectId, fieldTypes);
|
|
35
35
|
if (siteContactIds && siteContactIds.length === 0)
|
|
36
36
|
return [];
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return [];
|
|
37
|
+
// Paged and loud (#3674): an unpaged read stopped at 1,000 rows, and an error read as "no one".
|
|
38
|
+
const data = await readAllRows(filtered);
|
|
40
39
|
const contacts = data.filter((contact) => evaluateSegmentRules(contact, tree, fieldTypes));
|
|
41
40
|
let ids = contacts.map((contact) => String(contact.id));
|
|
42
41
|
if (tree.exclude_suppressed !== false && ids.length > 0) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
.select('email')
|
|
46
|
-
.eq('project_id', projectId);
|
|
47
|
-
if (suppressed?.length) {
|
|
42
|
+
const suppressed = await readAllRows(db().from('suppressions').select('id, email').eq('project_id', projectId));
|
|
43
|
+
if (suppressed.length) {
|
|
48
44
|
const suppressedEmails = new Set(suppressed.map((row) => row.email));
|
|
49
45
|
ids = contacts
|
|
50
46
|
.filter((contact) => !suppressedEmails.has(String(contact.email)))
|
package/build/tools/segments.js
CHANGED
|
@@ -3,7 +3,7 @@ import Anthropic from '@anthropic-ai/sdk';
|
|
|
3
3
|
import { db } from '../db.js';
|
|
4
4
|
import { getProjectId } from '../project.js';
|
|
5
5
|
import { getAnthropicModelId } from '../anthropic-model.js';
|
|
6
|
-
import { applySegmentRules, buildSegmentBaseQuery, evaluateSegmentRules, normalizeSegmentRulesToTree, } from '@sendinel/domain';
|
|
6
|
+
import { applySegmentRules, buildSegmentBaseQuery, evaluateSegmentRules, normalizeSegmentRulesToTree, readAllRows, } from '@sendinel/domain';
|
|
7
7
|
async function loadFieldTypes(projectId) {
|
|
8
8
|
const client = db();
|
|
9
9
|
const scoped = typeof client.schema === 'function' ? client.schema('email') : client;
|
|
@@ -34,18 +34,14 @@ async function getSegmentContactIds(rules) {
|
|
|
34
34
|
const { query: filtered, siteContactIds } = await applySegmentRules(q, rules, db(), projectId, fieldTypes);
|
|
35
35
|
if (siteContactIds && siteContactIds.length === 0)
|
|
36
36
|
return [];
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return [];
|
|
37
|
+
// Paged and loud (#3674): an unpaged read stopped at 1,000 rows, and an error read as "no one".
|
|
38
|
+
const data = await readAllRows(filtered);
|
|
40
39
|
const contacts = data.filter((contact) => evaluateSegmentRules(contact, tree, fieldTypes));
|
|
41
40
|
let ids = contacts.map((c) => String(c.id));
|
|
42
41
|
if (tree.exclude_suppressed !== false && ids.length > 0) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
-
.eq('project_id', projectId);
|
|
47
|
-
if (suppressed?.length) {
|
|
48
|
-
const suppressedEmails = new Set(suppressed.map((s) => s.email));
|
|
42
|
+
const suppressed = await readAllRows(db().from('suppressions').select('id, email').eq('project_id', projectId));
|
|
43
|
+
if (suppressed.length) {
|
|
44
|
+
const suppressedEmails = new Set(suppressed.map((row) => row.email));
|
|
49
45
|
ids = contacts
|
|
50
46
|
.filter((c) => !suppressedEmails.has(String(c.email)))
|
|
51
47
|
.map((c) => String(c.id));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.30",
|
|
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",
|