@sendinel/mcp-server 1.0.94 → 1.0.96
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/README.md +2 -2
- package/build/lib/action-approvals.js +1 -0
- package/build/server.js +1 -1
- package/build/tools/campaigns.js +7 -4
- package/build/tools/contacts.js +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ AI-controlled email operations control plane for campaigns, contacts, segments,
|
|
|
12
12
|
|
|
13
13
|
## Tool groups
|
|
14
14
|
|
|
15
|
-
Sendinel currently exposes 35 tool groups and
|
|
15
|
+
Sendinel currently exposes 35 tool groups and 339 tools (gateway mode exposes 4 meta-tools, sharing how_to with full mode). These counts are checked against the tool registry in CI, so this table matches what ships.
|
|
16
16
|
|
|
17
17
|
| Group | Count | Purpose |
|
|
18
18
|
|-------|-------|---------|
|
|
@@ -28,7 +28,7 @@ Sendinel currently exposes 35 tool groups and 338 tools (gateway mode exposes 4
|
|
|
28
28
|
| `cloud-import` | 6 | Cloud storage import |
|
|
29
29
|
| `compound` | 4 | Higher-level campaign/template/site setup and delivery diagnosis workflows |
|
|
30
30
|
| `connector` | 6 | BYOD connector status, table discovery, schema/view setup, mapping, resync |
|
|
31
|
-
| `contacts` |
|
|
31
|
+
| `contacts` | 38 | Subscriber CRUD, saved views, tags, email log, suppression, merge, timeline |
|
|
32
32
|
| `content` | 14 | Content blocks, gated asset links, blog ingest and auto-publish, sources |
|
|
33
33
|
| `data` | 19 | Platform connectors, imports, API keys, migration jobs, notifications, tenant export |
|
|
34
34
|
| `data-proposals` | 3 | Data-derived campaign proposal generation, listing, approval |
|
|
@@ -53,6 +53,7 @@ const GATED_ACTIONS = new Set([
|
|
|
53
53
|
'revoke_api_key', // breaks every integration using the key
|
|
54
54
|
'tenant_export_full', // copies all customer data out of Sendinel
|
|
55
55
|
'enroll_contact', // starts real emails to a contact
|
|
56
|
+
'identify_subscriber', // may enroll a new signup in sending campaigns
|
|
56
57
|
'enroll_automation', // starts automated messages to a contact
|
|
57
58
|
'enroll_in_escalation_ladder', // starts email/SMS escalation to a contact
|
|
58
59
|
'set_contact_sms_consent', // changes the consent record that allows texting
|
package/build/server.js
CHANGED
|
@@ -82,7 +82,7 @@ export const ALL_TOOL_GROUPS = Object.keys(GROUP_REGISTRY);
|
|
|
82
82
|
const WRITE_TOOLS = new Set([
|
|
83
83
|
'recheck_domain_dns',
|
|
84
84
|
'mark_agent_message', 'draft_agent_reply', 'create_agent_mailbox',
|
|
85
|
-
'add_subscriber', 'import_subscribers', 'update_subscriber', 'bulk_update_subscribers',
|
|
85
|
+
'add_subscriber', 'identify_subscriber', 'import_subscribers', 'update_subscriber', 'bulk_update_subscribers',
|
|
86
86
|
'create_subscriber_view', 'update_subscriber_view', 'delete_subscriber_view',
|
|
87
87
|
'enrich_subscriber',
|
|
88
88
|
'create_campaign', 'update_campaign', 'delete_campaign', 'add_campaign_step', 'update_campaign_status',
|
package/build/tools/campaigns.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CAMPAIGN_BRANCH_CONDITIONS, campaignBranchConditionValue } from "@sendinel/domain";
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
import Anthropic from '@anthropic-ai/sdk';
|
|
3
4
|
import { assertSendAccepted, SendRefusedError, BLOCKING_CONTENT_ISSUE_KINDS, computeSendContentKey, findContentIssues, sendingHostOf, NAME_FALLBACKS, ONE_SEND_MATCH_STATUSES, ONE_SEND_SENT_STATUSES, ONE_SEND_WINDOW_DAYS, normalizeTriggerConfig, renderTemplateString as renderTemplateVars, resolveBrandTokens, brandWarnings, readAllRows, } from '@sendinel/domain';
|
|
@@ -296,7 +297,6 @@ function normalizeConsistencyResult(value) {
|
|
|
296
297
|
// app/api/dashboard/campaigns/goals/route.ts
|
|
297
298
|
// If those lists change, change these. A value MCP accepts that REST rejects (or the
|
|
298
299
|
// reverse) is a divergence between two front doors onto the same engine.
|
|
299
|
-
const CAMPAIGN_BRANCH_CONDITIONS = ['tag_added', 'lifecycle_reached', 'conversion', 'link_clicked', 'custom_event', 'reply', 'click', 'booking'];
|
|
300
300
|
const CAMPAIGN_BRANCH_ACTIONS = ['goto_step', 'goto_campaign', 'exit', 'complete'];
|
|
301
301
|
const CAMPAIGN_GOAL_TYPES = ['conversion', 'tag_added', 'lifecycle_reached', 'link_clicked', 'custom_event'];
|
|
302
302
|
const CAMPAIGN_GOAL_ACTIONS = ['exit', 'complete', 'move_to_campaign'];
|
|
@@ -3537,18 +3537,21 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3537
3537
|
server.registerTool('add_campaign_branch', {
|
|
3538
3538
|
title: 'Add Campaign Branch',
|
|
3539
3539
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
3540
|
-
description: 'Add an if/then rule to a campaign step so the sequence branches on what the contact does. Use this for conditional logic like: if they clicked the link send a different follow-up, if they replied skip ahead, if a tag was added move them to another campaign, otherwise continue. When the condition matches, the branch action runs instead of continuing to the next step. Lower priority is evaluated first.',
|
|
3540
|
+
description: 'Add an if/then rule to a campaign step so the sequence branches on what the contact does or their current custom properties. Use this for conditional logic like: if they clicked the link send a different follow-up, if they replied skip ahead, if a tag was added move them to another campaign, otherwise continue. When the condition matches, the branch action runs instead of continuing to the next step. Lower priority is evaluated first.',
|
|
3541
3541
|
inputSchema: {
|
|
3542
3542
|
campaign_id: z.string().uuid(),
|
|
3543
3543
|
from_step_id: z.string().uuid().describe('The step this branch leaves from'),
|
|
3544
3544
|
condition_type: z.enum(CAMPAIGN_BRANCH_CONDITIONS).describe('What triggers the branch'),
|
|
3545
|
-
condition_value: z.string().optional().describe('
|
|
3545
|
+
condition_value: z.union([z.string(), z.record(z.string(), z.unknown())]).optional().describe('Tag, URL or event name for existing conditions. For contact_property: {key, op: eq|neq, value: string} or {key, op: exists}; key matches [A-Za-z0-9_.-]{1,64}. Missing/null properties never match.'),
|
|
3546
3546
|
action: z.enum(CAMPAIGN_BRANCH_ACTIONS).describe('goto_step and goto_campaign require their matching target'),
|
|
3547
3547
|
target_step_id: z.string().uuid().optional().describe('Required when action is goto_step'),
|
|
3548
3548
|
target_campaign_id: z.string().uuid().optional().describe('Required when action is goto_campaign'),
|
|
3549
3549
|
priority: z.number().int().min(0).optional().default(0).describe('Lower runs first (default 0)'),
|
|
3550
3550
|
},
|
|
3551
3551
|
}, async ({ campaign_id, from_step_id, condition_type, condition_value, action, target_step_id, target_campaign_id, priority }) => {
|
|
3552
|
+
const conditionValue = campaignBranchConditionValue(condition_type, condition_value);
|
|
3553
|
+
if (!conditionValue.ok)
|
|
3554
|
+
return { ...j({ error: conditionValue.error, code: 'INVALID_CONDITION_VALUE' }), isError: true };
|
|
3552
3555
|
if (!(await campaignInProject(campaign_id)))
|
|
3553
3556
|
return j({ error: 'Campaign not found in this project', code: 'CAMPAIGN_NOT_FOUND' });
|
|
3554
3557
|
// Same requirement the REST route enforces: an action naming a target must have one,
|
|
@@ -3562,7 +3565,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3562
3565
|
campaign_id,
|
|
3563
3566
|
from_step_id,
|
|
3564
3567
|
condition_type,
|
|
3565
|
-
condition_value:
|
|
3568
|
+
condition_value: conditionValue.value,
|
|
3566
3569
|
action,
|
|
3567
3570
|
target_step_id: action === 'goto_step' ? target_step_id : null,
|
|
3568
3571
|
target_campaign_id: action === 'goto_campaign' ? target_campaign_id : null,
|
package/build/tools/contacts.js
CHANGED
|
@@ -3,6 +3,7 @@ import { CONTACT_ENRICHMENT_PROVIDERS } from '@desler/models';
|
|
|
3
3
|
import { createContactEnrichmentAdapters, enrichContactFromConnections, } from '@desler/models/contact-enrichment';
|
|
4
4
|
import { buildPreviewAttentionEnvelope, applySegmentRules, ContactPropertyAnomalyLogger, normalizeContactProperties, getAttentionDecision, getContactNeighborhood, getScheduledTouches, findContactDuplicateGroups, readAllPages, readAllRows, chunkIds, } from '@sendinel/domain';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
+
import { callSendinelV1 } from '../lib/v1-client.js';
|
|
6
7
|
import { db, projectClientForWriteback } from '../db.js';
|
|
7
8
|
import { getProjectId } from '../project.js';
|
|
8
9
|
import { checkPlanLimit } from '../plan-limits.js';
|
|
@@ -535,6 +536,25 @@ async function exportSubscriberData(projectId, contactId) {
|
|
|
535
536
|
};
|
|
536
537
|
}
|
|
537
538
|
export function registerContactTools(server) {
|
|
539
|
+
server.registerTool('identify_subscriber', {
|
|
540
|
+
title: 'Identify Subscriber',
|
|
541
|
+
description: 'Identify or update a contact through the v1 identify API. Optional signup/customer consent subscribes new or existing contacts without overriding opt-outs. Customer consent requires a topic; consent requires site_id. Without consent, site_id retains legacy create-only subscription behavior and returns a warning.',
|
|
542
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
543
|
+
inputSchema: {
|
|
544
|
+
approval_id: z.string().uuid().optional().describe('Call once to request approval, then repeat the same arguments with the approved approval_id.'),
|
|
545
|
+
email: z.string().email(),
|
|
546
|
+
first_name: z.string().optional(),
|
|
547
|
+
last_name: z.string().optional(),
|
|
548
|
+
properties: z.record(z.string(), z.unknown()).optional(),
|
|
549
|
+
tags: z.array(z.string()).optional(),
|
|
550
|
+
site_id: z.string().uuid().optional(),
|
|
551
|
+
consent: z.enum(['signup', 'customer']).optional(),
|
|
552
|
+
topic: z.string().regex(/^[a-z0-9_]{1,64}$/).optional(),
|
|
553
|
+
},
|
|
554
|
+
}, withApproval('identify_subscriber', async (params) => {
|
|
555
|
+
const result = await callSendinelV1('/api/v1/identify', { method: 'POST', body: params });
|
|
556
|
+
return result.ok ? j(result.data) : err(JSON.stringify(result.error));
|
|
557
|
+
}));
|
|
538
558
|
server.registerTool('get_subscriber_timeline', {
|
|
539
559
|
title: 'Get Subscriber Timeline',
|
|
540
560
|
annotations: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendinel/mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.96",
|
|
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",
|