@sendinel/mcp-server 1.0.39 → 1.0.41
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 +45 -13
- package/package.json +1 -1
package/build/tools/contacts.js
CHANGED
|
@@ -2109,17 +2109,16 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2109
2109
|
confidence_threshold: z.number().min(0.5).max(1).optional().describe('Minimum similarity (0.5-1.0, default 0.8)'),
|
|
2110
2110
|
limit: z.number().int().min(1).max(200).optional().describe('Max duplicate groups (default 50)'),
|
|
2111
2111
|
site_id: z.string().uuid().optional().describe('Filter to contacts subscribed to this site'),
|
|
2112
|
+
include_test: z.boolean().optional().describe('Include is_test (seed/fixture) contacts. Default false, matching the dashboard contact list.'),
|
|
2112
2113
|
},
|
|
2113
|
-
}, async ({ confidence_threshold = 0.8, limit = 50, site_id }) => {
|
|
2114
|
+
}, async ({ confidence_threshold = 0.8, limit = 50, site_id, include_test = false }) => {
|
|
2114
2115
|
const projectId = getProjectId();
|
|
2115
2116
|
const maxGroups = Math.min(limit, 200);
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
.order('created_at', { ascending: true })
|
|
2122
|
-
.limit(2000);
|
|
2117
|
+
const CONTACT_SCAN_LIMIT = 2000;
|
|
2118
|
+
// #3604: PostgREST returns at most 1,000 rows per request, so a single
|
|
2119
|
+
// .limit(2000) query only ever read 1,000. Read in pages up to the limit.
|
|
2120
|
+
const SCAN_PAGE = 1000;
|
|
2121
|
+
let siteIds = null;
|
|
2123
2122
|
if (site_id) {
|
|
2124
2123
|
const { data: subs } = await db()
|
|
2125
2124
|
.from('site_subscriptions')
|
|
@@ -2128,14 +2127,47 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2128
2127
|
.eq('subscribed', true);
|
|
2129
2128
|
const ids = (subs ?? []).map((s) => s.contact_id);
|
|
2130
2129
|
if (!ids.length)
|
|
2131
|
-
return j({ duplicate_groups: [], total_groups: 0 });
|
|
2132
|
-
|
|
2130
|
+
return j({ duplicate_groups: [], total_groups: 0, coverage: { contacts_scanned: 0, contacts_matching: 0, contact_limit: CONTACT_SCAN_LIMIT, complete: true } });
|
|
2131
|
+
siteIds = ids;
|
|
2132
|
+
}
|
|
2133
|
+
const pageQuery = (from) => {
|
|
2134
|
+
let q = db()
|
|
2135
|
+
.from('subscribers')
|
|
2136
|
+
.select('id, email, first_name, last_name, engagement_score, lifecycle_stage, created_at, source', { count: 'exact' })
|
|
2137
|
+
.eq('project_id', projectId)
|
|
2138
|
+
.eq('global_unsubscribed', false);
|
|
2139
|
+
// #3604: hide seed/fixture rows like every other default contact view.
|
|
2140
|
+
if (!include_test)
|
|
2141
|
+
q = q.not('is_test', 'is', true);
|
|
2142
|
+
if (siteIds)
|
|
2143
|
+
q = q.in('id', siteIds);
|
|
2144
|
+
return q.order('created_at', { ascending: true }).order('id', { ascending: true })
|
|
2145
|
+
.range(from, Math.min(from + SCAN_PAGE, CONTACT_SCAN_LIMIT) - 1);
|
|
2146
|
+
};
|
|
2147
|
+
const contacts = [];
|
|
2148
|
+
let count = null;
|
|
2149
|
+
let error = null;
|
|
2150
|
+
for (let from = 0; from < CONTACT_SCAN_LIMIT; from += SCAN_PAGE) {
|
|
2151
|
+
const res = await pageQuery(from);
|
|
2152
|
+
if (res.error) {
|
|
2153
|
+
error = res.error;
|
|
2154
|
+
break;
|
|
2155
|
+
}
|
|
2156
|
+
if (from === 0)
|
|
2157
|
+
count = typeof res.count === 'number' ? res.count : null;
|
|
2158
|
+
const rows = res.data ?? [];
|
|
2159
|
+
contacts.push(...rows);
|
|
2160
|
+
if (rows.length < SCAN_PAGE)
|
|
2161
|
+
break;
|
|
2133
2162
|
}
|
|
2134
|
-
const { data: contacts, error } = await q;
|
|
2135
2163
|
if (error)
|
|
2136
2164
|
return txt(`Error: ${error.message}`);
|
|
2165
|
+
// #3604: say when the scan stopped at the limit instead of presenting a partial scan as complete.
|
|
2166
|
+
const scanned = contacts?.length ?? 0;
|
|
2167
|
+
const matching = typeof count === 'number' ? count : null;
|
|
2168
|
+
const coverage = { contacts_scanned: scanned, contacts_matching: matching, contact_limit: CONTACT_SCAN_LIMIT, complete: matching === null ? null : matching <= scanned };
|
|
2137
2169
|
if (!contacts?.length)
|
|
2138
|
-
return j({ duplicate_groups: [], total_groups: 0 });
|
|
2170
|
+
return j({ duplicate_groups: [], total_groups: 0, coverage });
|
|
2139
2171
|
const lev = (a, b) => {
|
|
2140
2172
|
const m = a.length, n = b.length;
|
|
2141
2173
|
const dp = Array.from({ length: m + 1 }, (_, i) => i);
|
|
@@ -2211,7 +2243,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2211
2243
|
}
|
|
2212
2244
|
groups.sort((a, b) => b.confidence - a.confidence);
|
|
2213
2245
|
const result = groups.slice(0, maxGroups);
|
|
2214
|
-
return j({ duplicate_groups: result, total_groups: result.length });
|
|
2246
|
+
return j({ duplicate_groups: result, total_groups: result.length, coverage });
|
|
2215
2247
|
});
|
|
2216
2248
|
server.registerTool('merge_subscribers', {
|
|
2217
2249
|
title: 'Merge Subscribers',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.41",
|
|
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",
|