@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.
- package/build/tools/contacts.js +36 -6
- package/package.json +1 -1
package/build/tools/contacts.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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",
|