@sendinel/mcp-server 1.0.21 → 1.0.23
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 +37 -72
- package/package.json +1 -1
package/build/tools/contacts.js
CHANGED
|
@@ -2283,82 +2283,47 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
2283
2283
|
},
|
|
2284
2284
|
}, async ({ site_id }) => {
|
|
2285
2285
|
const projectId = getProjectId();
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
});
|
|
2291
|
-
if (error) {
|
|
2292
|
-
// Fallback: inline query if RPC not yet available
|
|
2293
|
-
const SCHEMA_SAMPLE_LIMIT = 500;
|
|
2294
|
-
async function sampleSubscriberProperties(scopeToSite) {
|
|
2295
|
-
let query = db()
|
|
2296
|
-
.from('subscribers')
|
|
2297
|
-
.select('custom_properties')
|
|
2298
|
-
.eq('project_id', projectId)
|
|
2299
|
-
.not('custom_properties', 'is', null)
|
|
2300
|
-
.order('created_at', { ascending: false });
|
|
2301
|
-
if (scopeToSite)
|
|
2302
|
-
query = query.eq('source_site', site_id);
|
|
2303
|
-
return query.limit(SCHEMA_SAMPLE_LIMIT);
|
|
2304
|
-
}
|
|
2305
|
-
const siteSample = await sampleSubscriberProperties(true);
|
|
2306
|
-
if (siteSample.error)
|
|
2307
|
-
return txt(`Error: ${siteSample.error.message}`);
|
|
2308
|
-
let rows = siteSample.data ?? [];
|
|
2309
|
-
let scope = 'site';
|
|
2310
|
-
if (rows.length === 0) {
|
|
2311
|
-
const projectSample = await sampleSubscriberProperties(false);
|
|
2312
|
-
if (projectSample.error)
|
|
2313
|
-
return txt(`Error: ${projectSample.error.message}`);
|
|
2314
|
-
rows = projectSample.data ?? [];
|
|
2315
|
-
scope = 'project';
|
|
2316
|
-
}
|
|
2317
|
-
const keyMap = new Map();
|
|
2318
|
-
for (const row of rows ?? []) {
|
|
2319
|
-
const props = row.custom_properties;
|
|
2320
|
-
if (!props)
|
|
2321
|
-
continue;
|
|
2322
|
-
for (const [k, v] of Object.entries(props)) {
|
|
2323
|
-
if (!keyMap.has(k))
|
|
2324
|
-
keyMap.set(k, { values: new Set(), count: 0 });
|
|
2325
|
-
const entry = keyMap.get(k);
|
|
2326
|
-
entry.count++;
|
|
2327
|
-
if (v !== null && v !== undefined && entry.values.size < 3) {
|
|
2328
|
-
entry.values.add(String(v));
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2331
|
-
}
|
|
2332
|
-
const schema = Array.from(keyMap.entries())
|
|
2333
|
-
.sort((a, b) => b[1].count - a[1].count)
|
|
2334
|
-
.map(([key, { values, count }]) => ({
|
|
2335
|
-
key,
|
|
2336
|
-
merge_tag: `{{custom.${key}}}`,
|
|
2337
|
-
merge_tag_with_fallback: `{{custom.${key}|fallback}}`,
|
|
2338
|
-
example_values: Array.from(values),
|
|
2339
|
-
contacts_with_this_key: count,
|
|
2340
|
-
}));
|
|
2341
|
-
return j({
|
|
2342
|
-
site_id,
|
|
2343
|
-
scope,
|
|
2344
|
-
total_keys: schema.length,
|
|
2345
|
-
schema,
|
|
2346
|
-
note: scope === 'project'
|
|
2347
|
-
? `No contacts are tagged to site ${site_id} via source_site — sampled custom_properties across the whole project instead. Use merge_tag syntax in email body/subject. For sensitive data, add a fallback: {{custom.plan|subscriber}}. ⚠️ custom_properties data passes through the Anthropic API — avoid storing sensitive PII (SSNs, passwords, financial account numbers).`
|
|
2348
|
-
: 'Use merge_tag syntax in email body/subject. For sensitive data, add a fallback: {{custom.plan|subscriber}}. ⚠️ custom_properties data passes through the Anthropic API — avoid storing sensitive PII (SSNs, passwords, financial account numbers).',
|
|
2286
|
+
async function loadSchemaKeys(siteScope) {
|
|
2287
|
+
const { data, error } = await db().rpc('get_contact_schema_keys', {
|
|
2288
|
+
p_project_id: projectId,
|
|
2289
|
+
...(siteScope ? { p_site_id: siteScope } : {}),
|
|
2349
2290
|
});
|
|
2291
|
+
if (error)
|
|
2292
|
+
return { error: `Error: ${error.message}` };
|
|
2293
|
+
const result = (Array.isArray(data) ? data[0] : data);
|
|
2294
|
+
if (typeof result?.has_membership !== 'boolean' || !Array.isArray(result.keys)) {
|
|
2295
|
+
return { error: 'Error: get_contact_schema_keys returned an unreadable result' };
|
|
2296
|
+
}
|
|
2297
|
+
return result;
|
|
2350
2298
|
}
|
|
2299
|
+
// Site membership is hybrid (#3489): subscribers.source_site OR a site_subscriptions
|
|
2300
|
+
// row, counted once. A real site whose members have no custom_properties yet is still
|
|
2301
|
+
// site-scoped; project scope is only for a site with no members at all.
|
|
2302
|
+
let loaded = await loadSchemaKeys(site_id);
|
|
2303
|
+
if ('error' in loaded)
|
|
2304
|
+
return txt(loaded.error);
|
|
2305
|
+
let scope = 'site';
|
|
2306
|
+
if (!loaded.has_membership) {
|
|
2307
|
+
loaded = await loadSchemaKeys(null);
|
|
2308
|
+
if ('error' in loaded)
|
|
2309
|
+
return txt(loaded.error);
|
|
2310
|
+
scope = 'project';
|
|
2311
|
+
}
|
|
2312
|
+
const schema = loaded.keys.map(({ key, contacts_with_this_key, example_values }) => ({
|
|
2313
|
+
key,
|
|
2314
|
+
merge_tag: `{{custom.${key}}}`,
|
|
2315
|
+
merge_tag_with_fallback: `{{custom.${key}|fallback}}`,
|
|
2316
|
+
example_values,
|
|
2317
|
+
contacts_with_this_key,
|
|
2318
|
+
}));
|
|
2351
2319
|
return j({
|
|
2352
2320
|
site_id,
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
contacts_with_this_key: r.count,
|
|
2360
|
-
})),
|
|
2361
|
-
note: 'Use merge_tag syntax in email body/subject. For sensitive data, add a fallback: {{custom.plan|subscriber}}. ⚠️ custom_properties data passes through the Anthropic API — avoid storing sensitive PII (SSNs, passwords, financial account numbers).',
|
|
2321
|
+
scope,
|
|
2322
|
+
total_keys: schema.length,
|
|
2323
|
+
schema,
|
|
2324
|
+
note: scope === 'project'
|
|
2325
|
+
? `No contacts are linked to site ${site_id} via source_site or site_subscriptions — read custom_properties across the whole project instead. Use merge_tag syntax in email body/subject. For sensitive data, add a fallback: {{custom.plan|subscriber}}. ⚠️ custom_properties data passes through the Anthropic API — avoid storing sensitive PII (SSNs, passwords, financial account numbers).`
|
|
2326
|
+
: 'Use merge_tag syntax in email body/subject. For sensitive data, add a fallback: {{custom.plan|subscriber}}. ⚠️ custom_properties data passes through the Anthropic API — avoid storing sensitive PII (SSNs, passwords, financial account numbers).',
|
|
2362
2327
|
});
|
|
2363
2328
|
});
|
|
2364
2329
|
server.registerTool('create_custom_field', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
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",
|