conduyt-mcp 4.17.0 → 4.19.0

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/dist/client.js CHANGED
@@ -150,6 +150,14 @@ export class ConduytClient {
150
150
  const json = JSON.parse(text);
151
151
  if (json?.error)
152
152
  msg = json.error;
153
+ // Structured error details (e.g. STAGE_REQUIREMENTS carries
154
+ // lostReasonOptions) must reach the agent, or a "must be one of the
155
+ // configured options" error is unactionable. Bounded so a pathological
156
+ // body can't bloat the message.
157
+ const details = Object.fromEntries(Object.entries(json ?? {}).filter(([k]) => k !== "error"));
158
+ if (Object.keys(details).length > 0) {
159
+ msg = `${msg} ${JSON.stringify(details).slice(0, 1000)}`;
160
+ }
153
161
  }
154
162
  catch {
155
163
  if (text)
@@ -104,6 +104,7 @@ export function registerDealTools(server, client) {
104
104
  expectedCloseDate: z.string().optional().describe("Expected close date (ISO 8601)"),
105
105
  assignedTo: z.string().optional().describe("Reassign to user (owner) UUID"),
106
106
  source: z.string().trim().min(1).max(500).nullable().optional().describe("Attribution source for THIS deal (the trigger that produced it) — distinct from the contact's first-touch source. Pass null to clear it; a blank string is rejected. 1-500 chars."),
107
+ lostReason: z.string().optional().describe("Reason for a lost/disqualified deal. Some stages REQUIRE this on entry (the move fails with code STAGE_REQUIREMENTS listing lostReasonOptions when the stage has a configured dropdown — resend with one of those exact values)."),
107
108
  customFields: z.record(z.any()).optional().describe("Structured custom-field values to merge, keyed by field key"),
108
109
  }, async ({ id, ...updates }) => {
109
110
  const result = await client.patch(`/api/v1/deals/${id}`, updates);
@@ -22,8 +22,23 @@ import { ConduytClient } from "../client.js";
22
22
  * - 503 — the shared email-provider allowance is busy with customer sends.
23
23
  * Retry in a moment; it says nothing about the domain's health.
24
24
  *
25
- * Removal is IRREVERSIBLE for in-flight threads: replies to emails already
26
- * sent go to an address that no longer routes, so they are lost. Hence the
27
- * confirm gate below.
25
+ * Removal semantics (#39 drain, shipped 2026-08-07) branch by state:
26
+ * - LIVE (provider-backed): DRAIN. Sending stops immediately, the
27
+ * address keeps receiving for a 30-day window (late replies still
28
+ * land on the timeline), then it parks. The name is permanently
29
+ * retired — no account can ever set it up again.
30
+ * - LIVE legacy row with no stored provider id: immediate removal, NO
31
+ * drain (threading stops at once); providerCleanup='manual' and the
32
+ * name stays reusable.
33
+ * - DEGRADED (provider reports it can no longer receive): same drain
34
+ * lifecycle and same permanent retirement, but NO delivery guarantee
35
+ * during the window — receiving was already broken.
36
+ * - PENDING setup, a domain the provider has already lost, or a cleanup
37
+ * retry: immediate teardown. The registry row is deleted and the name
38
+ * is NOT retired — it can be reused.
39
+ * Sending-domain removal: every capture row EXCEPT a released tombstone
40
+ * blocks it. Pending/cleanup rows clear through this same remove tool
41
+ * (self-service); draining/parked rows keep blocking until an operator
42
+ * releases them after the drain.
28
43
  */
29
44
  export declare function registerReplyCaptureTools(server: McpServer, client: ConduytClient): void;
@@ -22,16 +22,31 @@ import { formatResult } from "../client.js";
22
22
  * - 503 — the shared email-provider allowance is busy with customer sends.
23
23
  * Retry in a moment; it says nothing about the domain's health.
24
24
  *
25
- * Removal is IRREVERSIBLE for in-flight threads: replies to emails already
26
- * sent go to an address that no longer routes, so they are lost. Hence the
27
- * confirm gate below.
25
+ * Removal semantics (#39 drain, shipped 2026-08-07) branch by state:
26
+ * - LIVE (provider-backed): DRAIN. Sending stops immediately, the
27
+ * address keeps receiving for a 30-day window (late replies still
28
+ * land on the timeline), then it parks. The name is permanently
29
+ * retired — no account can ever set it up again.
30
+ * - LIVE legacy row with no stored provider id: immediate removal, NO
31
+ * drain (threading stops at once); providerCleanup='manual' and the
32
+ * name stays reusable.
33
+ * - DEGRADED (provider reports it can no longer receive): same drain
34
+ * lifecycle and same permanent retirement, but NO delivery guarantee
35
+ * during the window — receiving was already broken.
36
+ * - PENDING setup, a domain the provider has already lost, or a cleanup
37
+ * retry: immediate teardown. The registry row is deleted and the name
38
+ * is NOT retired — it can be reused.
39
+ * Sending-domain removal: every capture row EXCEPT a released tombstone
40
+ * blocks it. Pending/cleanup rows clear through this same remove tool
41
+ * (self-service); draining/parked rows keep blocking until an operator
42
+ * releases them after the drain.
28
43
  */
29
44
  export function registerReplyCaptureTools(server, client) {
30
- server.tool("conduyt_reply_capture_get", "Get the account's reply-capture state: whether it is configured and live, the capture domain and reply address, DNS records for a pending setup, and provider health. health='checking' with a healthWarning means the provider is re-checking DNS — capture stays active. degraded=true means the provider can no longer receive on that domain and it must be removed and set up again. needsSupport=true means an operator must reconcile it first. cleanupPending=true means a previous removal did not finish at the provider — call conduyt_reply_capture_remove again to complete it.", {}, async () => {
45
+ server.tool("conduyt_reply_capture_get", "Get the account's reply-capture state: whether it is configured and live, the capture domain and reply address, DNS records for a pending setup, and provider health. health='checking' with a healthWarning means the provider is re-checking DNS — capture stays active. degraded=true means the provider can no longer receive on that domain and it must be removed and set up again. needsSupport=true means an operator must reconcile it first. cleanupPending=true means a previous removal did not finish at the provider — call conduyt_reply_capture_remove again to complete it. After a removal: draining=true (with drainEndsAt) means the drain window is open — a domain that was live at removal keeps receiving replies until it closes (one that was degraded has no such guarantee); parked=true means it is retired and inbound-inert. Either way that name is permanently unavailable — use suggestedDomain when setting up again.", {}, async () => {
31
46
  const result = await client.get("/api/v1/email-domains/reply-capture");
32
47
  return formatResult(result);
33
48
  });
34
- server.tool("conduyt_reply_capture_setup", "Start reply-capture setup: creates the receiving domain at the email provider and returns the DNS records to add. The domain MUST be a strict subdomain of the account's own verified sending domain (omit it to use the suggested 'reply.<sending-domain>'); anything else is refused. Requires a verified sending domain first. After adding the DNS records, call conduyt_reply_capture_verify. A 409 means capture is already configured, a setup is already in progress, or the previous one still needs operator reconciliation.", {
49
+ server.tool("conduyt_reply_capture_setup", "Start reply-capture setup: creates the receiving domain at the email provider and returns the DNS records to add. The domain MUST be a strict subdomain of the account's own verified sending domain (omit it to use the suggested 'reply.<sending-domain>'); anything else is refused. Requires a verified sending domain first. After adding the DNS records, call conduyt_reply_capture_verify. A 409 means capture is already configured, a setup is already in progress, the previous one still needs operator reconciliation, or the requested name belongs to a retired (drained/parked) domain — retired names are permanently unavailable, so read suggestedDomain from conduyt_reply_capture_get (e.g. 'reply2.<domain>') instead of retrying the old name.", {
35
50
  domain: z
36
51
  .string()
37
52
  // TRIM BEFORE the emptiness check: the API trims and treats a blank
@@ -52,10 +67,10 @@ export function registerReplyCaptureTools(server, client) {
52
67
  const result = await client.post("/api/v1/email-domains/reply-capture/verify", {});
53
68
  return formatResult(result);
54
69
  });
55
- server.tool("conduyt_reply_capture_remove", "Remove reply capture: stops using the capture Reply-To on new email, stops matching inbound replies, and deletes the receiving domain at the provider. Replies to emails ALREADY SENT will be lost there is no undo. Also completes a previously interrupted removal (cleanupPending) without creating anything new. Requires an explicit confirm='REMOVE'. Note: while any reply-capture row exists the sending domain cannot be removed, so this is also the step that unblocks that.", {
70
+ server.tool("conduyt_reply_capture_remove", "Remove reply capture. Outcome depends on the domain's state. LIVE with a provider-backed setup: it DRAINS the capture Reply-To stops on new email immediately, but the address keeps receiving for 30 days (drainEndsAt in the response) so late replies to already-sent emails still reach the contact's timeline; after that it parks, and the name is permanently retired (no account can ever set it up again — resuming capture means a NEW subdomain, see suggestedDomain on conduyt_reply_capture_get). LIVE legacy row with no stored provider id: removed immediately with NO drain — reply threading stops at once and the response says providerCleanup='manual' (an operator finishes the provider side); the name stays reusable. DEGRADED: drain lifecycle and permanent retirement, but receiving was already broken, so do NOT promise the 30-day delivery window. PENDING setup, a domain the provider has already lost, or an interrupted-removal retry (cleanupPending): torn down immediately, the row is deleted, and the name stays reusable. Requires an explicit confirm='REMOVE'. Sending-domain removal stays blocked by every capture row except a released tombstone pending/cleanup rows clear right here, but a draining/parked row keeps blocking until support releases it after the drain.", {
56
71
  confirm: z
57
72
  .literal("REMOVE")
58
- .describe("Must be exactly 'REMOVE'. Hard safety gate: replies to already-sent emails are irrecoverably lost once the capture domain is gone."),
73
+ .describe("Must be exactly 'REMOVE'. Hard safety gate: sending stops immediately, and a provider-backed live or degraded domain's name is permanently retired — there is no undo on the retirement (a legacy id-less live row instead stops threading at once)."),
59
74
  }, async () => {
60
75
  const result = await client.del("/api/v1/email-domains/reply-capture");
61
76
  return formatResult(result);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt-mcp",
3
- "version": "4.17.0",
3
+ "version": "4.19.0",
4
4
  "description": "MCP server for Conduyt CRM — expose CRM operations as AI-accessible tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",