@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.
@@ -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
- // Sample recent contacts to discover keys (LIMIT 500 is enough for schema discovery)
2287
- const { data, error } = await db().rpc('get_contact_schema_keys', {
2288
- p_project_id: projectId,
2289
- p_site_id: site_id,
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
- total_keys: data.length,
2354
- schema: data.map(r => ({
2355
- key: r.key,
2356
- merge_tag: `{{custom.${r.key}}}`,
2357
- merge_tag_with_fallback: `{{custom.${r.key}|fallback}}`,
2358
- example_values: r.examples ?? [],
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.21",
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",