@sendinel/mcp-server 1.0.22 → 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.
@@ -2283,54 +2283,38 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
2283
2283
  },
2284
2284
  }, async ({ site_id }) => {
2285
2285
  const projectId = getProjectId();
2286
- // Sample recent contacts to discover keys (LIMIT 500 is enough for schema discovery)
2287
- const SCHEMA_SAMPLE_LIMIT = 500;
2288
- async function sampleSubscriberProperties(scopeToSite) {
2289
- let query = db()
2290
- .from('subscribers')
2291
- .select('custom_properties')
2292
- .eq('project_id', projectId)
2293
- .not('custom_properties', 'is', null)
2294
- .order('created_at', { ascending: false });
2295
- if (scopeToSite)
2296
- query = query.eq('source_site', site_id);
2297
- return query.limit(SCHEMA_SAMPLE_LIMIT);
2298
- }
2299
- const siteSample = await sampleSubscriberProperties(true);
2300
- if (siteSample.error)
2301
- return txt(`Error: ${siteSample.error.message}`);
2302
- let rows = siteSample.data ?? [];
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 } : {}),
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;
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);
2303
2305
  let scope = 'site';
2304
- if (rows.length === 0) {
2305
- const projectSample = await sampleSubscriberProperties(false);
2306
- if (projectSample.error)
2307
- return txt(`Error: ${projectSample.error.message}`);
2308
- rows = projectSample.data ?? [];
2306
+ if (!loaded.has_membership) {
2307
+ loaded = await loadSchemaKeys(null);
2308
+ if ('error' in loaded)
2309
+ return txt(loaded.error);
2309
2310
  scope = 'project';
2310
2311
  }
2311
- const keyMap = new Map();
2312
- for (const row of rows ?? []) {
2313
- const props = row.custom_properties;
2314
- if (!props)
2315
- continue;
2316
- for (const [k, v] of Object.entries(props)) {
2317
- if (!keyMap.has(k))
2318
- keyMap.set(k, { values: new Set(), count: 0 });
2319
- const entry = keyMap.get(k);
2320
- entry.count++;
2321
- if (v !== null && v !== undefined && entry.values.size < 3) {
2322
- entry.values.add(String(v));
2323
- }
2324
- }
2325
- }
2326
- const schema = Array.from(keyMap.entries())
2327
- .sort((a, b) => b[1].count - a[1].count)
2328
- .map(([key, { values, count }]) => ({
2312
+ const schema = loaded.keys.map(({ key, contacts_with_this_key, example_values }) => ({
2329
2313
  key,
2330
2314
  merge_tag: `{{custom.${key}}}`,
2331
2315
  merge_tag_with_fallback: `{{custom.${key}|fallback}}`,
2332
- example_values: Array.from(values),
2333
- contacts_with_this_key: count,
2316
+ example_values,
2317
+ contacts_with_this_key,
2334
2318
  }));
2335
2319
  return j({
2336
2320
  site_id,
@@ -2338,7 +2322,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
2338
2322
  total_keys: schema.length,
2339
2323
  schema,
2340
2324
  note: scope === 'project'
2341
- ? `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).`
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).`
2342
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).',
2343
2327
  });
2344
2328
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.22",
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",