conduyt-mcp 4.72.0 → 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.
@@ -20,7 +20,7 @@ const settingsFields = {
20
20
  };
21
21
  const uuid = (what) => z.string().uuid().describe(what);
22
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; 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 paused or cancelled run with an endedReason has ended.";
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
24
  const stepSchema = z.object({
25
25
  templateId: z.string().uuid().describe("An active template of the sequence's channel (Email Templates or SMS Templates)"),
26
26
  waitDays: z.number().int().min(0).optional().describe("Days after the previous step (the first step sends at enrollment)"),
@@ -118,11 +118,18 @@ export function registerSequenceTools(server, client) {
118
118
  });
119
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
120
  sequenceId: uuid("Sequence UUID"),
121
- status: z.enum(["active", "paused", "completed", "failed", "cancelled", "unsubscribed"]).optional().describe("Only runs with this status"),
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"),
123
- page: z.number().optional().describe("Page number (default 1)"),
124
- per_page: z.number().optional().describe("Results per page (default 50, max 200)"),
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
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
+ }
126
133
  const params = new URLSearchParams();
127
134
  if (status)
128
135
  params.set("status", status);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt-mcp",
3
- "version": "4.72.0",
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",