conduyt-mcp 4.71.1 → 4.72.1

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  MCP server for [Conduyt CRM](https://conduyt.app) — expose your CRM as AI-accessible tools. Covers the core CRM surface (contacts, deals, pipelines, custom fields, automations, messaging, and more); use `conduyt_api_catalog` to discover the full set of 500+ REST endpoints.
4
4
 
5
- ## 292 Tools
5
+ ## 299 Tools
6
6
 
7
7
  ### Discovery
8
8
  | Tool | Description |
@@ -74,14 +74,20 @@ MCP server for [Conduyt CRM](https://conduyt.app) — expose your CRM as AI-acce
74
74
  | `conduyt_workflow_simulate` | Dry-run a workflow against a real or stub contact — step-by-step trace with compliance and provider checks |
75
75
  | `conduyt_workflow_promote` | Safely publish a workflow — validates first, refuses if errors exist |
76
76
 
77
- ### Email Sequences
77
+ ### Sequences (Email and SMS)
78
78
  | Tool | Description |
79
79
  |------|-------------|
80
- | `conduyt_list_sequences` | List email sequences with enrollment stats |
81
- | `conduyt_get_sequence` | Get sequence with steps and stats |
82
- | `conduyt_create_sequence` | Create a new email sequence with steps |
83
- | `conduyt_enroll_in_sequence` | Enroll contacts into a sequence |
84
- | `conduyt_unenroll_from_sequence` | Remove a contact from a sequence |
80
+ | `conduyt_list_sequences` | List Email and SMS sequences: filter by type, On/Off, archived; view=summary for a light row each |
81
+ | `conduyt_get_sequence` | One sequence: steps, sender, send window, stop rules, enrollment switches, people per run status |
82
+ | `conduyt_create_sequence` | Create an Email or SMS sequence from saved templates (type fixed at creation, created Off unless isActive) |
83
+ | `conduyt_update_sequence` | Change steps and settings, turn On or Off, archive or restore |
84
+ | `conduyt_list_sequence_senders` | Who can be a sequence's sender, per channel (address for email, assigned line for SMS) |
85
+ | `conduyt_preflight_sequence_enrollment` | Check contacts before enrolling: eligible, skipped and why, blockers |
86
+ | `conduyt_pick_sequence_list` | Who of a Smart View or Smart List goes in next, with a cursor to continue |
87
+ | `conduyt_enroll_in_sequence` | Enroll contacts into a sequence (up to 500 per call) |
88
+ | `conduyt_unenroll_from_sequence` | Remove a contact from a sequence (the run ends as removed) |
89
+ | `conduyt_list_sequence_enrollments` | The runs in a sequence: status, endedReason, current step, engagement; all_active for every live run |
90
+ | `conduyt_get_sequence_stats` | Sends, opens and clicks in total and per step |
85
91
 
86
92
  ### Messaging
87
93
  | Tool | Description |
@@ -91,6 +97,7 @@ MCP server for [Conduyt CRM](https://conduyt.app) — expose your CRM as AI-acce
91
97
  | `conduyt_whatsapp_templates` | The account's WhatsApp templates (Twilio Content) with Meta's approval verdict: sid, text with {{n}} placeholders, variables, approval |
92
98
  | `conduyt_send_sms` | Send SMS to a contact (max 1600 chars); transport twilio, project_blue, or whatsapp (an approved template by Content SID, or free-form within the 24-hour window) |
93
99
  | `conduyt_send_email` | Send a single email to a contact |
100
+ | `conduyt_log_message` | Record a message on a contact's timeline; an inbound SMS STOP or START records the number's opt-out or opt-in and answers smsConsent |
94
101
 
95
102
  ### Companies
96
103
  | Tool | Description |
@@ -26,7 +26,7 @@ export function registerAutomationTools(server, client) {
26
26
  kind: z.enum(["native", "n8n"]).optional().describe("Automation type — 'native' for the built-in graph engine (use `actions`), 'n8n' for an external n8n webhook (requires n8nWorkflowId + n8nWebhookUrl). For agent-built automations use 'native'."),
27
27
  n8nWorkflowId: z.string().optional().describe("n8n workflow ID (required when kind='n8n')"),
28
28
  n8nWebhookUrl: z.string().optional().describe("n8n webhook URL (required when kind='n8n')"),
29
- actions: z.any().optional().describe("Actions graph — v1: [{type, config}], v2: {nodes:[], startNodeId}"),
29
+ actions: z.any().optional().describe("Actions graph — v1: [{type, config}], v2: {nodes:[], startNodeId}. A sequence step is the action enroll_in_sequence with config {sequenceId, enrollAction: 'add' (default) | 'remove'}: add puts the contact into the sequence (the sequence must allow automation enrollment, allowAutomationEnroll), remove ends their run."),
30
30
  graphVersion: z.number().optional().describe("Graph version — 1 (legacy array) or 2 (node graph). Default 1"),
31
31
  schedule: z.string().optional().describe("5-field cron expression (required if triggerEvent='scheduled')"),
32
32
  scheduleTimezone: z.string().optional().describe("IANA timezone for scheduled triggers (e.g. 'America/Los_Angeles')"),
@@ -48,7 +48,7 @@ export function registerAutomationTools(server, client) {
48
48
  .optional()
49
49
  .describe("Updated trigger conditions (replaces the object). Same keys as create — e.g. { untouchedDays: 7 } on 'contact.untouched'. (#16 tri-surface parity: the UI and API could set this; MCP could not.)"),
50
50
  isActive: z.boolean().optional().describe("Enable or disable the automation"),
51
- actions: z.any().optional().describe("Updated actions graph"),
51
+ actions: z.any().optional().describe("Updated actions graph (same shape as create; e.g. enroll_in_sequence {sequenceId, enrollAction: 'add' | 'remove'})"),
52
52
  schedule: z.string().optional().describe("Updated cron schedule"),
53
53
  scheduleTimezone: z.string().optional().describe("Updated timezone"),
54
54
  stopOnResponse: z.boolean().optional().describe("Stop drip if contact responds"),
@@ -88,6 +88,24 @@ export function registerMessagingTools(server, client) {
88
88
  const result = await client.get(`/api/v1/messages?${params}`);
89
89
  return formatResult(result);
90
90
  });
91
+ server.tool("conduyt_log_message", "Record a message on a contact's timeline (POST /messages): an inbound text or email that arrived through another system, or an outbound email (sent through the workspace's verified domain). Outbound SMS is refused here unless status is 'draft': use conduyt_send_sms. An inbound SMS whose body is STOP (or START) with a fromNumber records that number's opt-out (or opt-in), stops the contact's sequences and drips on STOP, exactly like a text that came in through a provider; the 201 then carries smsConsent {keyword: stop | start, fromNumber, applied, reason?}: applied true when the number's consent changed, else reason number_not_on_contact (the number is not the contact's primary or secondary number: only the message is stored) or number_not_canonicalizable (the number could not be read). Requires communications:create.", {
92
+ contactId: z.string().uuid().describe("Contact UUID"),
93
+ channel: z.enum(["sms", "email"]).describe("Channel"),
94
+ direction: z.enum(["inbound", "outbound"]).describe("inbound = from the contact, outbound = to the contact"),
95
+ body: z.string().describe("Message body (max 100KB)"),
96
+ subject: z.string().max(500).optional().describe("Subject (email)"),
97
+ fromNumber: z.string().max(30).optional().describe("SMS: the sending number (for an inbound STOP/START, the contact's number whose consent changes)"),
98
+ toNumber: z.string().max(30).optional().describe("SMS: the receiving number"),
99
+ fromEmail: z.string().email().optional().describe("Email: the sender address"),
100
+ toEmail: z.string().email().optional().describe("Email: the recipient address (outbound: must be the contact's address)"),
101
+ provider: z.string().optional().describe("The system the message came through"),
102
+ providerId: z.string().optional().describe("That system's id for the message"),
103
+ status: z.string().optional().describe("Message status, e.g. 'draft'"),
104
+ scheduledAt: z.string().optional().describe("ISO date-time to send an outbound email later (needs a verified sending domain)"),
105
+ }, async (params) => {
106
+ const result = await client.post("/api/v1/messages", params);
107
+ return formatResult(result);
108
+ });
91
109
  server.tool("conduyt_sms_delivery_report", "SMS provider delivery-outcome report for external-workflow SMS drips: per-automation entered vs delivered / rejected (invalid, litigator, DNC) / provider-error / workflow dead-end, plus a send-attempt 'why rejected' reason breakdown (line_type = 'not mobile' often WRONGLY rejected; landline_skipped / no_phone_skipped = Conduyt's own recorded skips). Use to answer 'what did the SMS provider do with our drips?'", {
92
110
  from: z.string().optional().describe("ISO start (omit both for the last 180 days; spans clamp to 180d)"),
93
111
  to: z.string().optional().describe("ISO end"),
@@ -1,14 +1,48 @@
1
1
  import { z } from "zod";
2
2
  import { formatResult } from "../client.js";
3
+ /**
4
+ * Sequences (one model for Email and SMS since 2026-09-30): steps send saved templates of the sequence's channel a
5
+ * number of days apart inside a send window; a run stops on a reply, a booked meeting, an opt-out, a closed deal or a
6
+ * removal. Provider drip timetables (BDA) are a different thing: see the drip-campaign tools.
7
+ */
8
+ const settingsFields = {
9
+ senderUserId: z.string().uuid().nullable().optional().describe("The team member the messages are from (must be an active member): email from their on-domain address, SMS from their assigned line. null = the workspace default"),
10
+ autoUnenrollOnReply: z.boolean().optional().describe("Stop the contact's run when they reply to any message of the sequence (default true)"),
11
+ autoUnenrollOnMeeting: z.boolean().optional().describe("Stop the run when a meeting is booked through any Conduyt calendar (default true)"),
12
+ autoUnenrollOnUnsubscribe: z.boolean().optional().describe("Stop the run when the contact opts out: unsubscribe, or STOP for SMS (default true)"),
13
+ autoUnenrollOnBounce: z.boolean().optional().describe("Email only. Stop the run on a hard bounce; a soft bounce waits and retries (default true)"),
14
+ autoUnenrollOnDealClosed: z.boolean().optional().describe("Stop the contact's run when any of their deals closes, won or lost, on any stage of a closed kind (default true)"),
15
+ allowAutomationEnroll: z.boolean().optional().describe("Let the automation action enroll_in_sequence put contacts in (default true)"),
16
+ allowReenroll: z.boolean().optional().describe("Let a contact go through it again once their earlier run ENDED (default false). A live or paused run is never restarted"),
17
+ weekdaysOnly: z.boolean().optional().describe("Send Monday to Friday only, in the contact's time zone (default true)"),
18
+ sendStartHour: z.number().int().min(0).max(23).optional().describe("First hour of the send window, local (default 9)"),
19
+ sendEndHour: z.number().int().min(1).max(24).optional().describe("End of the send window, exclusive (default 17). A step due outside the window waits for the next opening"),
20
+ };
21
+ const uuid = (what) => z.string().uuid().describe(what);
22
+ /** how a run ended (enrollment endedReason), and the run statuses, as GET /emails/sequences/{id}/enrollments answers them */
23
+ const RUN_READING = "Each run carries status (active | paused | completed | failed | cancelled | unsubscribed) and endedReason (completed | replied | meeting | opted_out | bounced | deal_closed | removed | failed | no_phone; null while the run is live). A paused run with no endedReason is LIVE (the sequence is Off or archived; it resumes when turned On); a failed run with no endedReason is ALSO LIVE (an ambiguous send outcome — a timeout, an unknown provider answer — that a late provider acceptance can still revive in place), while failed WITH endedReason \"failed\" is TERMINAL (a definitive worker or repair failure, no revival). no_phone is SMS-only: the run ended because the contact had no usable phone. A paused, failed or cancelled run with an endedReason has ended.";
24
+ const stepSchema = z.object({
25
+ templateId: z.string().uuid().describe("An active template of the sequence's channel (Email Templates or SMS Templates)"),
26
+ waitDays: z.number().int().min(0).optional().describe("Days after the previous step (the first step sends at enrollment)"),
27
+ });
3
28
  export function registerSequenceTools(server, client) {
4
- server.tool("conduyt_list_sequences", "List email sequences (drip campaigns). Returns name, step count, enrollment stats, and active status.", {
5
- is_active: z.boolean().optional().describe("Filter by active/inactive status"),
29
+ server.tool("conduyt_list_sequences", "List sequences (Email and SMS), newest first. Each carries type (email | sms), isActive (On/Off), steps, senderUserId and sender, the stop rules (autoUnenrollOnReply, autoUnenrollOnMeeting, autoUnenrollOnUnsubscribe, autoUnenrollOnBounce, autoUnenrollOnDealClosed), allowAutomationEnroll, allowReenroll, weekdaysOnly, sendStartHour, sendEndHour, archivedAt, and the people in it now (_count.enrollments). Archived sequences are left out unless archived=true. An API key sees only the channel(s) it is scoped to (a type filter outside its scope is a 403).", {
30
+ type: z.enum(["email", "sms"]).optional().describe("Only sequences of this channel"),
31
+ is_active: z.boolean().optional().describe("Only sequences that are On (true) or Off (false)"),
32
+ archived: z.boolean().optional().describe("true lists archived sequences only"),
33
+ view: z.enum(["summary"]).optional().describe("summary = a light row per sequence (id, name, type, isActive, createdAt, updatedAt, enrolledCount, description cut to 300 characters, stepCount), no step JSON"),
6
34
  page: z.number().optional().describe("Page number (default 1)"),
7
35
  per_page: z.number().optional().describe("Results per page (default 50, max 200)"),
8
- }, async ({ is_active, page, per_page }) => {
36
+ }, async ({ type, is_active, archived, view, page, per_page }) => {
9
37
  const params = new URLSearchParams();
38
+ if (type)
39
+ params.set("type", type);
10
40
  if (is_active !== undefined)
11
41
  params.set("is_active", String(is_active));
42
+ if (archived !== undefined)
43
+ params.set("archived", String(archived));
44
+ if (view)
45
+ params.set("view", view);
12
46
  if (page)
13
47
  params.set("page", String(page));
14
48
  if (per_page)
@@ -16,38 +50,102 @@ export function registerSequenceTools(server, client) {
16
50
  const result = await client.get(`/api/v1/emails/sequences?${params}`);
17
51
  return formatResult(result);
18
52
  });
19
- server.tool("conduyt_get_sequence", "Get a single email sequence with full details — steps, enrollment stats, and configuration.", {
20
- id: z.string().describe("Sequence UUID"),
53
+ server.tool("conduyt_get_sequence", "Get a single sequence with full details: type, isActive, steps (template per step, wait in days), senderUserId and sender, send window (weekdaysOnly, sendStartHour, sendEndHour), stop rules, allowAutomationEnroll, allowReenroll, archivedAt, the people in it now (_count.enrollments) and enrollmentStats (people per run status).", {
54
+ id: uuid("Sequence UUID"),
21
55
  }, async ({ id }) => {
22
56
  const result = await client.get(`/api/v1/emails/sequences/${id}`);
23
57
  return formatResult(result);
24
58
  });
25
- server.tool("conduyt_create_sequence", "Create a new email sequence with steps. Each step has a delay and email content.", {
59
+ server.tool("conduyt_create_sequence", "Create an Email or SMS sequence from saved templates. Every step names a template of the sequence's channel and a wait in days; the type cannot change afterwards. Created Off unless isActive is true (turning it On checks the steps; 422 lists what is wrong). Runs stop on a reply, a booked meeting, an opt-out (STOP for SMS), a closed deal (autoUnenrollOnDealClosed) or a removal.", {
26
60
  name: z.string().describe("Sequence name"),
27
61
  description: z.string().optional().describe("Sequence description"),
28
- isActive: z.boolean().optional().describe("Whether the sequence is active (default true)"),
29
- steps: z.array(z.object({
30
- delay: z.number().optional().describe("Delay in seconds before sending this step"),
31
- body: z.string().optional().describe("Plain text email body"),
32
- bodyHtml: z.string().optional().describe("HTML email body"),
33
- templateId: z.string().optional().describe("Email template UUID to use instead of inline body"),
34
- })).optional().describe("Sequence steps in order"),
62
+ type: z.enum(["email", "sms"]).optional().describe("The channel (default email). Immutable after creation"),
63
+ isActive: z.boolean().optional().describe("On (true) or Off (false, the default)"),
64
+ steps: z.array(stepSchema).min(1).describe("Steps in order"),
65
+ ...settingsFields,
35
66
  }, async (params) => {
36
67
  const result = await client.post("/api/v1/emails/sequences", params);
37
68
  return formatResult(result);
38
69
  });
39
- server.tool("conduyt_enroll_in_sequence", "Enroll one or more contacts into an email sequence. Skips contacts already enrolled.", {
40
- sequenceId: z.string().describe("Sequence UUID"),
41
- contactIds: z.array(z.string()).describe("Array of contact UUIDs to enroll"),
70
+ server.tool("conduyt_update_sequence", "Update a sequence: name, steps, sender, send window, stop rules, enrollment switches; turn it On or Off (isActive); archive it (archived: true turns it Off and hides it) or restore it (archived: false; it stays Off until turned On). The type cannot change (400 when sent). While people are in it, the steps they already received cannot be changed, removed or reordered (409 sequence_steps_in_use); later steps change and new ones append.", {
71
+ id: uuid("Sequence UUID"),
72
+ name: z.string().optional(),
73
+ description: z.string().optional(),
74
+ isActive: z.boolean().optional().describe("On (true) or Off (false). Off keeps every run where it is; the steps resume when turned On"),
75
+ archived: z.boolean().optional().describe("true = Off and hidden from lists; false = restored"),
76
+ steps: z.array(stepSchema).min(1).optional().describe("The whole step list, in order (replaces the current steps)"),
77
+ ...settingsFields,
78
+ }, async ({ id, ...updates }) => {
79
+ const result = await client.patch(`/api/v1/emails/sequences/${id}`, updates);
80
+ return formatResult(result);
81
+ });
82
+ server.tool("conduyt_list_sequence_senders", "Who can be a sequence's sender, for one channel: every active team member who accepted their invite. channel sms lists each member's assigned line (did, null when none: an SMS sequence needs a sender with a line); channel email lists each member's address. Use an id here as senderUserId.", {
83
+ channel: z.enum(["email", "sms"]).describe("The sequence's channel"),
84
+ }, async ({ channel }) => {
85
+ const result = await client.get(`/api/v1/emails/sequences/senders?channel=${channel}`);
86
+ return formatResult(result);
87
+ });
88
+ server.tool("conduyt_preflight_sequence_enrollment", "Check contacts BEFORE enrolling them: who is eligible and who would be skipped, with the reason (no email or phone for the channel, opted out, already in this sequence, already went through it, a bounced address, not in your scope). Returns total, eligible, skipped, errors (blockers: the sequence is Off or archived, a template is missing, the sending domain is not verified; any blocker makes nobody eligible), warnings, missingTemplates, domainReady and contacts [{contactId, eligible, reasons}]. Nothing is written.", {
89
+ sequenceId: uuid("Sequence UUID"),
90
+ contactIds: z.array(z.string().uuid()).min(1).max(500).describe("Contact UUIDs to check (up to 500)"),
91
+ }, async ({ sequenceId, contactIds }) => {
92
+ const result = await client.post(`/api/v1/emails/sequences/${sequenceId}/preflight`, { contactIds });
93
+ return formatResult(result);
94
+ });
95
+ server.tool("conduyt_pick_sequence_list", "Who of a saved list (a Smart View or a Smart List) goes into a sequence next: the server walks the list in a stable order, leaves out the contacts already in the sequence, judges the rest with the enrollment check, and returns up to `limit` eligible contacts plus a cursor to continue. Returns listTotal, scanned, alreadyIn, contacts [{id, firstName, lastName, email, phone}], ineligible [{contactId, reasons}], blockers (Off, archived, a missing template, an unverified domain: no contacts are returned while any stands), cursor and exhausted. Enroll the returned ids with conduyt_enroll_in_sequence, then call again with the cursor until exhausted. Needs the contacts and communications scopes; Smart Lists are admin-only. Nothing is written.", {
96
+ sequenceId: uuid("Sequence UUID"),
97
+ smartViewId: z.string().uuid().optional().describe("The Smart View to pick from (one of smartViewId / smartListId)"),
98
+ smartListId: z.string().uuid().optional().describe("The Smart List to pick from"),
99
+ limit: z.number().int().min(1).max(500).optional().describe("Eligible contacts to return (default 500)"),
100
+ cursor: z.string().optional().describe("Where to continue, from the previous call's `cursor`"),
101
+ }, async ({ sequenceId, smartViewId, smartListId, limit, cursor }) => {
102
+ const result = await client.post(`/api/v1/emails/sequences/${sequenceId}/pick-from-list`, { smartViewId, smartListId, limit, cursor });
103
+ return formatResult(result);
104
+ });
105
+ server.tool("conduyt_enroll_in_sequence", "Enroll one or more contacts into a sequence (up to 500 per call; the sequence must be On and not archived, else 404). Skips contacts with no address for the channel, opted out, or already in it; with allowReenroll on, a contact whose earlier run ENDED starts over (a live or paused run never restarts). Returns enrolled, skipped, duplicates, total, skippedDetails [{contactId, reasons}], and outOfScope / staleAfterPreflight when they apply. Check first with conduyt_preflight_sequence_enrollment.", {
106
+ sequenceId: uuid("Sequence UUID"),
107
+ contactIds: z.array(z.string().uuid()).min(1).max(500).describe("Array of contact UUIDs to enroll"),
42
108
  }, async ({ sequenceId, contactIds }) => {
43
109
  const result = await client.post(`/api/v1/emails/sequences/${sequenceId}/enroll`, { contactIds });
44
110
  return formatResult(result);
45
111
  });
46
- server.tool("conduyt_unenroll_from_sequence", "Remove a contact from an email sequence. Pauses their enrollment.", {
47
- sequenceId: z.string().describe("Sequence UUID"),
48
- contactId: z.string().describe("Contact UUID to unenroll"),
112
+ server.tool("conduyt_unenroll_from_sequence", "Remove a contact from a sequence (one contact per call). Their live, paused or failed run ends: status cancelled, endedReason removed, no more steps; it can start over later only if the sequence allows re-enrollment. Returns contactId, unenrolled (false when the run had already ended) and currentStatus.", {
113
+ sequenceId: uuid("Sequence UUID"),
114
+ contactId: z.string().uuid().describe("Contact UUID to unenroll"),
49
115
  }, async ({ sequenceId, contactId }) => {
50
116
  const result = await client.post(`/api/v1/emails/sequences/${sequenceId}/unenroll`, { contactId });
51
117
  return formatResult(result);
52
118
  });
119
+ server.tool("conduyt_list_sequence_enrollments", `List the people in a sequence, newest run first: each run's contact, currentStep, nextSendAt, enrolledAt, completedAt, lastError and engagement (sent, opened, clicked). ${RUN_READING}`, {
120
+ sequenceId: uuid("Sequence UUID"),
121
+ status: z.enum(["active", "paused", "completed", "failed", "cancelled", "unsubscribed"]).optional().describe("Only runs with this status. Refused together with all_active=true — see all_active"),
122
+ all_active: z.boolean().optional().describe("true = EVERY live run (active, paused with no endedReason, failed with no endedReason) in one answer, no paging and no engagement; refused (422 too_many_live_runs) past 5,000. Pass it ALONE: status, page and per_page are refused together with all_active=true, because a status filter or pagination would silently cut the live set a rollback page relies on being complete"),
123
+ page: z.number().optional().describe("Page number (default 1). Refused together with all_active=true — see all_active"),
124
+ per_page: z.number().optional().describe("Results per page (default 50, max 200). Refused together with all_active=true — see all_active"),
125
+ }, async ({ sequenceId, status, all_active, page, per_page }) => {
126
+ // (round 2 Codex finding, high) all_active=true promises the COMPLETE live set. Letting status or pagination
127
+ // ride along would let the live route intersect them and silently return a subset while the caller believes
128
+ // it holds every live run (the risk: a rollback/bulk-unenroll agent leaves contacts mid-sequence). Refuse
129
+ // locally, before any request, rather than letting the subset through.
130
+ if (all_active && (status !== undefined || page !== undefined || per_page !== undefined)) {
131
+ throw new Error("all_active=true already returns every live run in one answer — pass it alone. status, page and per_page are refused together with all_active=true because they would cut the set instead of returning the complete one.");
132
+ }
133
+ const params = new URLSearchParams();
134
+ if (status)
135
+ params.set("status", status);
136
+ if (all_active !== undefined)
137
+ params.set("all_active", String(all_active));
138
+ if (page)
139
+ params.set("page", String(page));
140
+ if (per_page)
141
+ params.set("per_page", String(per_page));
142
+ const result = await client.get(`/api/v1/emails/sequences/${sequenceId}/enrollments?${params}`);
143
+ return formatResult(result);
144
+ });
145
+ server.tool("conduyt_get_sequence_stats", "Performance of a sequence: totals {enrolled (runs started, all time), contacts (people in it now), sent, opened, clicked, loggedSends, openRate, clickRate}, perStep [{step, sent, opened, clicked, openRate, clickRate}] for the current steps, and removedSteps for sends from steps no longer in it. A send counts once the provider took it (sent, delivered, bounced, complained).", {
146
+ sequenceId: uuid("Sequence UUID"),
147
+ }, async ({ sequenceId }) => {
148
+ const result = await client.get(`/api/v1/emails/sequences/${sequenceId}/stats`);
149
+ return formatResult(result);
150
+ });
53
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt-mcp",
3
- "version": "4.71.1",
3
+ "version": "4.72.1",
4
4
  "mcpName": "com.conduyt/mcp",
5
5
  "description": "MCP server for Conduyt CRM — expose CRM operations as AI-accessible tools",
6
6
  "type": "module",
@@ -1,11 +1,11 @@
1
1
  {
2
- "generatedAt": "2026-09-30T16:40:04.197Z",
3
- "toolCount": 292,
4
- "totalEndpoints": 1031,
2
+ "generatedAt": "2026-10-02T16:12:21.781Z",
3
+ "toolCount": 299,
4
+ "totalEndpoints": 1036,
5
5
  "counts": {
6
- "covered": 266,
7
- "excluded": 311,
8
- "planned": 454,
6
+ "covered": 273,
7
+ "excluded": 313,
8
+ "planned": 450,
9
9
  "uncategorized": 0
10
10
  },
11
11
  "matrix": {
@@ -1801,6 +1801,14 @@
1801
1801
  "status": "excluded",
1802
1802
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1803
1803
  },
1804
+ "GET /api/v1/cron/process-sms-sequences": {
1805
+ "status": "excluded",
1806
+ "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1807
+ },
1808
+ "POST /api/v1/cron/process-sms-sequences": {
1809
+ "status": "excluded",
1810
+ "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1811
+ },
1804
1812
  "GET /api/v1/cron/process-subscriber-deals": {
1805
1813
  "status": "excluded",
1806
1814
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
@@ -2652,8 +2660,10 @@
2652
2660
  ]
2653
2661
  },
2654
2662
  "PATCH /api/v1/emails/sequences/:id": {
2655
- "status": "planned",
2656
- "reason": "email template/sequence management"
2663
+ "status": "covered",
2664
+ "tools": [
2665
+ "conduyt_update_sequence"
2666
+ ]
2657
2667
  },
2658
2668
  "POST /api/v1/emails/sequences/:id/enroll": {
2659
2669
  "status": "covered",
@@ -2662,16 +2672,28 @@
2662
2672
  ]
2663
2673
  },
2664
2674
  "GET /api/v1/emails/sequences/:id/enrollments": {
2665
- "status": "planned",
2666
- "reason": "email template/sequence management"
2675
+ "status": "covered",
2676
+ "tools": [
2677
+ "conduyt_list_sequence_enrollments"
2678
+ ]
2679
+ },
2680
+ "POST /api/v1/emails/sequences/:id/pick-from-list": {
2681
+ "status": "covered",
2682
+ "tools": [
2683
+ "conduyt_pick_sequence_list"
2684
+ ]
2667
2685
  },
2668
2686
  "POST /api/v1/emails/sequences/:id/preflight": {
2669
- "status": "planned",
2670
- "reason": "email template/sequence management"
2687
+ "status": "covered",
2688
+ "tools": [
2689
+ "conduyt_preflight_sequence_enrollment"
2690
+ ]
2671
2691
  },
2672
2692
  "GET /api/v1/emails/sequences/:id/stats": {
2673
- "status": "planned",
2674
- "reason": "email template/sequence management"
2693
+ "status": "covered",
2694
+ "tools": [
2695
+ "conduyt_get_sequence_stats"
2696
+ ]
2675
2697
  },
2676
2698
  "POST /api/v1/emails/sequences/:id/unenroll": {
2677
2699
  "status": "covered",
@@ -2679,6 +2701,12 @@
2679
2701
  "conduyt_unenroll_from_sequence"
2680
2702
  ]
2681
2703
  },
2704
+ "GET /api/v1/emails/sequences/senders": {
2705
+ "status": "covered",
2706
+ "tools": [
2707
+ "conduyt_list_sequence_senders"
2708
+ ]
2709
+ },
2682
2710
  "GET /api/v1/emails/templates": {
2683
2711
  "status": "covered",
2684
2712
  "tools": [
@@ -3040,8 +3068,10 @@
3040
3068
  ]
3041
3069
  },
3042
3070
  "POST /api/v1/messages": {
3043
- "status": "planned",
3044
- "reason": "message surfaces beyond send tools"
3071
+ "status": "covered",
3072
+ "tools": [
3073
+ "conduyt_log_message"
3074
+ ]
3045
3075
  },
3046
3076
  "GET /api/v1/messages/:id/media/:mediaIndex": {
3047
3077
  "status": "planned",
@@ -4164,6 +4194,10 @@
4164
4194
  "status": "planned",
4165
4195
  "reason": "SMS surfaces beyond send tools"
4166
4196
  },
4197
+ "GET /api/v1/sms-providers/options": {
4198
+ "status": "planned",
4199
+ "reason": "SMS surfaces beyond send tools"
4200
+ },
4167
4201
  "GET /api/v1/sms/send-logs": {
4168
4202
  "status": "planned",
4169
4203
  "reason": "SMS surfaces beyond send tools"