@sendinel/mcp-server 1.0.20 → 1.0.21
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 +53 -4
- package/package.json +1 -1
package/build/tools/contacts.js
CHANGED
|
@@ -14,6 +14,7 @@ import { diffCustomProperties } from '../lib/custom-properties-diff.js';
|
|
|
14
14
|
import Anthropic from '@anthropic-ai/sdk';
|
|
15
15
|
import { getAnthropicModelId } from '../anthropic-model.js';
|
|
16
16
|
const txt = (text) => ({ content: [{ type: 'text', text }] });
|
|
17
|
+
const err = (text) => ({ ...txt(text), isError: true });
|
|
17
18
|
const j = (data) => txt(JSON.stringify(data, null, 2));
|
|
18
19
|
const CUSTOM_FIELD_TYPES = [
|
|
19
20
|
'email', 'text', 'textarea', 'select', 'checkbox', 'number', 'tel', 'url', 'date',
|
|
@@ -183,6 +184,19 @@ function normalizeCompanyDomain(value) {
|
|
|
183
184
|
function normalizeEmail(value) {
|
|
184
185
|
return String(value ?? '').trim().toLowerCase();
|
|
185
186
|
}
|
|
187
|
+
function normalizePhoneE164(value) {
|
|
188
|
+
const digits = String(value ?? '').replace(/[^\d+]/g, '');
|
|
189
|
+
if (!digits)
|
|
190
|
+
return null;
|
|
191
|
+
if (digits.startsWith('+1') && digits.length === 12)
|
|
192
|
+
return digits;
|
|
193
|
+
const numeric = digits.replace(/\D/g, '');
|
|
194
|
+
if (numeric.length === 10)
|
|
195
|
+
return `+1${numeric}`;
|
|
196
|
+
if (numeric.length === 11 && numeric.startsWith('1'))
|
|
197
|
+
return `+${numeric}`;
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
186
200
|
function isPlainObject(value) {
|
|
187
201
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
188
202
|
}
|
|
@@ -398,8 +412,8 @@ const SUBSCRIBER_SELECT_WITH_COMPANY = 'id, email, first_name, last_name, compan
|
|
|
398
412
|
const SUBSCRIBER_SELECT_NO_COMPANY = 'id, email, first_name, last_name, company_id, global_unsubscribed, source, created_at, last_emailed_at, total_emails_sent, total_opens, total_clicks, engagement_score, lifecycle_stage';
|
|
399
413
|
const SEGMENT_SUBSCRIBER_SELECT_WITH_COMPANY = 'id, email, first_name, last_name, company_id, company:companies(id, name, domain), engagement_score, lifecycle_stage, last_opened_at, last_clicked_at, total_opens, total_clicks';
|
|
400
414
|
const SEGMENT_SUBSCRIBER_SELECT_NO_COMPANY = 'id, email, first_name, last_name, company_id, engagement_score, lifecycle_stage, last_opened_at, last_clicked_at, total_opens, total_clicks';
|
|
401
|
-
const UPDATE_SUBSCRIBER_SELECT_WITH_COMPANY = 'id, email, first_name, last_name, company_id, company:companies(id, name, domain), custom_properties, tags';
|
|
402
|
-
const UPDATE_SUBSCRIBER_SELECT_NO_COMPANY = 'id, email, first_name, last_name, company_id, custom_properties, tags';
|
|
415
|
+
const UPDATE_SUBSCRIBER_SELECT_WITH_COMPANY = 'id, email, first_name, last_name, company_id, company:companies(id, name, domain), custom_properties, tags, phone_e164, timezone';
|
|
416
|
+
const UPDATE_SUBSCRIBER_SELECT_NO_COMPANY = 'id, email, first_name, last_name, company_id, custom_properties, tags, phone_e164, timezone';
|
|
403
417
|
const GET_SUBSCRIBER_SELECT_WITH_COMPANY = '*, company:companies(id, name, domain), site_subscriptions(site_id, subscribed)';
|
|
404
418
|
const GET_SUBSCRIBER_SELECT_NO_COMPANY = '*, site_subscriptions(site_id, subscribed)';
|
|
405
419
|
const CONTACT_ENRICHMENT_ADAPTERS = createContactEnrichmentAdapters({
|
|
@@ -1464,7 +1478,7 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1464
1478
|
idempotentHint: true,
|
|
1465
1479
|
openWorldHint: false,
|
|
1466
1480
|
},
|
|
1467
|
-
description: 'Update a contact\'s email, name, tags, or custom properties',
|
|
1481
|
+
description: 'Update a contact\'s email, name, phone, timezone, tags, or custom properties',
|
|
1468
1482
|
inputSchema: {
|
|
1469
1483
|
contact_id: z.string().uuid(),
|
|
1470
1484
|
email: z.string().email().optional(),
|
|
@@ -1475,8 +1489,10 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1475
1489
|
company_domain: z.string().nullable().optional(),
|
|
1476
1490
|
tags: z.array(z.string()).optional(),
|
|
1477
1491
|
custom_properties: z.record(z.string(), z.unknown()).optional().describe('Merged into existing custom_properties'),
|
|
1492
|
+
phone_e164: z.string().nullable().optional().describe('E.164 phone number, e.g. +15551234567. SMS is US-only for now.'),
|
|
1493
|
+
timezone: z.string().nullable().optional().describe('IANA time zone identifier, e.g. America/Los_Angeles'),
|
|
1478
1494
|
},
|
|
1479
|
-
}, async ({ contact_id, email, first_name, last_name, company_id, company_name, company_domain, tags, custom_properties }) => {
|
|
1495
|
+
}, async ({ contact_id, email, first_name, last_name, company_id, company_name, company_domain, tags, custom_properties, phone_e164, timezone }) => {
|
|
1480
1496
|
const projectId = getProjectId();
|
|
1481
1497
|
const embedOk = await hasCompaniesEmbed(projectId);
|
|
1482
1498
|
const updates = {};
|
|
@@ -1541,6 +1557,36 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1541
1557
|
...companyPatch,
|
|
1542
1558
|
};
|
|
1543
1559
|
}
|
|
1560
|
+
if (phone_e164 !== undefined) {
|
|
1561
|
+
if (phone_e164 === null) {
|
|
1562
|
+
updates.phone_e164 = null;
|
|
1563
|
+
}
|
|
1564
|
+
else {
|
|
1565
|
+
const normalized = normalizePhoneE164(phone_e164);
|
|
1566
|
+
if (!normalized)
|
|
1567
|
+
return err('Invalid phone_e164: must be a valid E.164 phone number (SMS is US-only for now).');
|
|
1568
|
+
updates.phone_e164 = normalized;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
if (timezone !== undefined) {
|
|
1572
|
+
if (timezone === null) {
|
|
1573
|
+
updates.timezone = null;
|
|
1574
|
+
}
|
|
1575
|
+
else {
|
|
1576
|
+
// Region form only; mirrors normalizeTimezoneField in the dashboard. Intl accepts UTC offsets
|
|
1577
|
+
// ("+05:00") and abbreviations ("EST"), which Postgres AT TIME ZONE reads differently.
|
|
1578
|
+
if (timezone !== 'UTC' && !/^[A-Za-z_]+(\/[A-Za-z0-9_+-]+)+$/.test(timezone)) {
|
|
1579
|
+
return err('Invalid timezone: must be an IANA region name such as America/Los_Angeles (not a UTC offset or abbreviation).');
|
|
1580
|
+
}
|
|
1581
|
+
try {
|
|
1582
|
+
new Intl.DateTimeFormat('en-US', { timeZone: timezone });
|
|
1583
|
+
}
|
|
1584
|
+
catch {
|
|
1585
|
+
return err('Invalid timezone: must be a valid IANA time zone identifier.');
|
|
1586
|
+
}
|
|
1587
|
+
updates.timezone = timezone;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1544
1590
|
if (!Object.keys(updates).length)
|
|
1545
1591
|
return txt('No fields to update.');
|
|
1546
1592
|
updates.updated_at = new Date().toISOString();
|
|
@@ -1551,6 +1597,9 @@ Requirements: urgency without desperation, acknowledge the gap, one clear CTA, v
|
|
|
1551
1597
|
.eq('project_id', projectId)
|
|
1552
1598
|
.select(embedOk ? UPDATE_SUBSCRIBER_SELECT_WITH_COMPANY : UPDATE_SUBSCRIBER_SELECT_NO_COMPANY)
|
|
1553
1599
|
.single();
|
|
1600
|
+
if (error?.code === '23505' && updates.phone_e164 !== undefined) {
|
|
1601
|
+
return err('Another contact in this project already has this phone number.');
|
|
1602
|
+
}
|
|
1554
1603
|
if (error)
|
|
1555
1604
|
return txt(`Error: ${error.message}`);
|
|
1556
1605
|
if (customPropertyChanges.length > 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.21",
|
|
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",
|