@sendinel/mcp-server 1.0.137 → 1.0.138

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.
@@ -1670,7 +1670,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
1670
1670
  idempotentHint: true,
1671
1671
  openWorldHint: false,
1672
1672
  },
1673
- description: 'Update a contact\'s email, name, phone, timezone, tags, or custom properties',
1673
+ description: 'Update a contact\'s email, name, phone, timezone, tags, or custom properties. tags replaces the whole tag set; use set_subscriber_tags to add tags.',
1674
1674
  inputSchema: {
1675
1675
  contact_id: z.string().uuid(),
1676
1676
  email: z.string().email().optional(),
@@ -1756,7 +1756,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
1756
1756
  else {
1757
1757
  const normalized = normalizePhoneE164(phone_e164);
1758
1758
  if (!normalized)
1759
- return err('Invalid phone_e164: must be a valid E.164 phone number (SMS is US-only for now).');
1759
+ return contactInputError('validation_failed', 'Invalid phone_e164: must be a valid E.164 phone number (SMS is US-only for now).');
1760
1760
  updates.phone_e164 = normalized;
1761
1761
  }
1762
1762
  }
@@ -1768,19 +1768,49 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
1768
1768
  // Region form only; mirrors normalizeTimezoneField in the dashboard. Intl accepts UTC offsets
1769
1769
  // ("+05:00") and abbreviations ("EST"), which Postgres AT TIME ZONE reads differently.
1770
1770
  if (timezone !== 'UTC' && !/^[A-Za-z_]+(\/[A-Za-z0-9_+-]+)+$/.test(timezone)) {
1771
- return err('Invalid timezone: must be an IANA region name such as America/Los_Angeles (not a UTC offset or abbreviation).');
1771
+ return contactInputError('validation_failed', 'Invalid timezone: must be an IANA region name such as America/Los_Angeles (not a UTC offset or abbreviation).');
1772
1772
  }
1773
1773
  try {
1774
1774
  new Intl.DateTimeFormat('en-US', { timeZone: timezone });
1775
1775
  }
1776
1776
  catch {
1777
- return err('Invalid timezone: must be a valid IANA time zone identifier.');
1777
+ return contactInputError('validation_failed', 'Invalid timezone: must be a valid IANA time zone identifier.');
1778
1778
  }
1779
1779
  updates.timezone = timezone;
1780
1780
  }
1781
1781
  }
1782
1782
  if (!Object.keys(updates).length)
1783
1783
  return txt('No fields to update.');
1784
+ const tagResult = {};
1785
+ if (tags !== undefined) {
1786
+ let previousTags;
1787
+ try {
1788
+ const { data: existing, error: readError } = await db()
1789
+ .from('subscribers')
1790
+ .select('tags')
1791
+ .eq('id', contact_id)
1792
+ .eq('project_id', projectId)
1793
+ .maybeSingle();
1794
+ if (!readError && Array.isArray(existing?.tags) && existing.tags.every((tag) => typeof tag === 'string')) {
1795
+ previousTags = existing.tags;
1796
+ }
1797
+ }
1798
+ catch {
1799
+ // The replacement can still succeed, but the previous set is unknown.
1800
+ }
1801
+ if (previousTags === undefined) {
1802
+ tagResult.warnings = ['Tags were REPLACED, but previous tags could not be read; removed and added tags are unknown. Use set_subscriber_tags to add tags without replacing the whole set.'];
1803
+ }
1804
+ else {
1805
+ const previousSet = new Set(previousTags);
1806
+ const nextSet = new Set(tags);
1807
+ tagResult.tags_removed = [...previousSet].filter((tag) => !nextSet.has(tag));
1808
+ tagResult.tags_added = [...nextSet].filter((tag) => !previousSet.has(tag));
1809
+ if (tagResult.tags_removed.length > 0) {
1810
+ tagResult.warnings = ['Tags were REPLACED. Use set_subscriber_tags to add tags without replacing the whole set.'];
1811
+ }
1812
+ }
1813
+ }
1784
1814
  updates.updated_at = new Date().toISOString();
1785
1815
  const { data, error } = await db()
1786
1816
  .from('subscribers')
@@ -1790,7 +1820,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
1790
1820
  .select(embedOk ? UPDATE_SUBSCRIBER_SELECT_WITH_COMPANY : UPDATE_SUBSCRIBER_SELECT_NO_COMPANY)
1791
1821
  .single();
1792
1822
  if (error?.code === '23505' && updates.phone_e164 !== undefined) {
1793
- return err('Another contact in this project already has this phone number.');
1823
+ return contactInputError('validation_failed', 'Another contact in this project already has this phone number.');
1794
1824
  }
1795
1825
  if (error)
1796
1826
  return txt(`Error: ${error.message}`);
@@ -1809,7 +1839,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
1809
1839
  ...(updates.tags !== undefined ? { tags: updates.tags } : {}),
1810
1840
  ...(updates.custom_properties !== undefined ? { custom_properties: updates.custom_properties } : {}),
1811
1841
  });
1812
- return j(data);
1842
+ return j({ ...data, ...tagResult });
1813
1843
  });
1814
1844
  server.registerTool('get_subscriber', {
1815
1845
  title: 'Get Subscriber',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sendinel/mcp-server",
3
- "version": "1.0.137",
3
+ "version": "1.0.138",
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",