conduyt-mcp 4.14.0 → 4.17.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/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
- ## 161 Tools
5
+ ## 173 Tools
6
6
 
7
7
  ### Discovery
8
8
  | Tool | Description |
@@ -258,6 +258,26 @@ MCP server for [Conduyt CRM](https://conduyt.app) — expose your CRM as AI-acce
258
258
  | `conduyt_export_contact_data` | GDPR data-portability export — full portable JSON of everything held about a contact (owner/admin) |
259
259
  | `conduyt_forget_contact` | IRREVERSIBLE GDPR right-to-be-forgotten erasure — requires `confirm='FORGET'` (owner only) |
260
260
 
261
+ ### Dialer
262
+ | Tool | Description |
263
+ |------|-------------|
264
+ | `conduyt_sync_local_presence` | Reconcile the Local Presence pool from Twilio (LP-named, voice, US +1, max 50) |
265
+
266
+ ### Lifecycle (master lead status + intake deals)
267
+ | Tool | Description |
268
+ |------|-------------|
269
+ | `conduyt_get_lifecycle_settings` | Effective master statuses (defaults + customs) and intake-deals config |
270
+ | `conduyt_update_intake_deals` | Configure automatic deal creation on live intake (webhooks, forms, bookings) |
271
+ | `conduyt_update_master_statuses` | Set the account's custom lead lifecycle statuses |
272
+
273
+ ### Reply Capture (inbound email)
274
+ | Tool | Description |
275
+ |------|-------------|
276
+ | `conduyt_reply_capture_get` | Current state: configured/live, capture domain + reply address, pending DNS records, provider health |
277
+ | `conduyt_reply_capture_setup` | Create the receiving subdomain (must be a subdomain of the verified sending domain) and return its DNS records |
278
+ | `conduyt_reply_capture_verify` | Check DNS and promote to live once the provider confirms inbound receiving |
279
+ | `conduyt_reply_capture_remove` | Remove capture (and finish an interrupted removal) — requires `confirm='REMOVE'`; replies to already-sent email are lost |
280
+
261
281
  ## Setup
262
282
 
263
283
  ```bash
package/dist/index.js CHANGED
@@ -36,6 +36,8 @@ import { registerAiExtendedTools } from "./tools/ai-extended.js";
36
36
  import { registerWorkflowSandboxTools } from "./tools/workflow-sandbox.js";
37
37
  import { registerPrivacyTools } from "./tools/privacy.js";
38
38
  import { registerAccountExportTools } from "./tools/account-exports.js";
39
+ import { registerLifecycleTools } from "./tools/lifecycle.js";
40
+ import { registerReplyCaptureTools } from "./tools/reply-capture.js";
39
41
  const apiUrl = process.env.CONDUYT_API_URL;
40
42
  const apiKey = process.env.CONDUYT_API_KEY;
41
43
  if (!apiUrl || !apiKey) {
@@ -94,5 +96,7 @@ registerAiExtendedTools(server, client);
94
96
  registerWorkflowSandboxTools(server, client);
95
97
  registerPrivacyTools(server, client);
96
98
  registerAccountExportTools(server, client);
99
+ registerLifecycleTools(server, client);
100
+ registerReplyCaptureTools(server, client);
97
101
  const transport = new StdioServerTransport();
98
102
  await server.connect(transport);
@@ -45,6 +45,10 @@ export function registerAutomationTools(server, client) {
45
45
  schedule: z.string().optional().describe("Updated cron schedule"),
46
46
  scheduleTimezone: z.string().optional().describe("Updated timezone"),
47
47
  stopOnResponse: z.boolean().optional().describe("Stop drip if contact responds"),
48
+ activeRunPolicy: z
49
+ .enum(["keep", "restart"])
50
+ .optional()
51
+ .describe("What a NEW trigger does when the contact is already mid-sequence: 'keep' (default — skip, existing run keeps its schedule) or 'restart' (cancel the parked run and re-enroll from step 1)"),
48
52
  }, async ({ id, ...updates }) => {
49
53
  const result = await client.patch(`/api/v1/automations/${id}`, updates);
50
54
  return formatResult(result);
@@ -46,4 +46,8 @@ export function registerCallTools(server, client) {
46
46
  const result = await client.get(`/api/v1/calls/${encodeURIComponent(id)}`);
47
47
  return formatResult(result);
48
48
  });
49
+ server.tool("conduyt_sync_local_presence", "Sync the dialer's Local Presence number pool from the account's Twilio subaccount. IMPORTANT — sync only adopts numbers that meet ALL of: (1) the Twilio FriendlyName contains a standalone 'LP' (or 'LocalPres'), e.g. 'LP Miami 305'; (2) the number is voice-capable; (3) it is a US +1 E.164 number. The first 50 matches in sorted order become the pool; anything already in the pool that no longer matches is REMOVED. So buying a number is not enough — name it with the LP convention in Twilio first, then run this. Returns pool/added/removed/matched. Requires settings:edit.", {}, async () => {
50
+ const result = await client.post("/api/v1/dialer/local-presence/sync", {});
51
+ return formatResult(result);
52
+ });
49
53
  }
@@ -52,7 +52,8 @@ export function registerContactTools(server, client) {
52
52
  lastName: z.string().optional().describe("Last name"),
53
53
  phone: z.string().optional().describe("Phone number"),
54
54
  company: z.string().optional().describe("Company name"),
55
- source: z.string().optional().describe("Lead source (e.g. 'website', 'referral', 'import')"),
55
+ source: z.string().trim().min(1).max(100).optional().describe("Lead source (e.g. 'website', 'referral', 'import') — FIRST-WRITE-WINS acquisition truth for the lead; later inbound events do not overwrite it. 1-100 chars."),
56
+ masterStatus: z.string().trim().min(1).max(40).optional().describe("Lifecycle status of the LEAD (master record), distinct from any deal's status: 'open' (default), 'won', 'lost', 'abandoned', 'disqualified', or an account-defined custom status. A terminal status (won/lost/abandoned/disqualified) SILENCES all outbound automations for this contact — drips, sequences, and scheduled sends stop. Use conduyt_get_lifecycle_settings to see the account's configured statuses."),
56
57
  tags: z.array(z.string()).optional().describe("Tag UUIDs to apply (tags must already exist — create them with conduyt_create_tag first)"),
57
58
  customFields: z.record(z.any()).optional().describe("Structured custom-field values keyed by field key, e.g. { lead_temperature: 'hot', budget: 50000 }. Define fields first with conduyt_create_custom_field (entity 'contact')."),
58
59
  custom_fields: z.record(z.any()).optional().describe("Alias for customFields (snake_case). Provide only one of customFields / custom_fields."),
@@ -69,6 +70,8 @@ export function registerContactTools(server, client) {
69
70
  lastName: z.string().optional().describe("Updated last name"),
70
71
  phone: z.string().optional().describe("Updated phone"),
71
72
  company: z.string().optional().describe("Updated company name"),
73
+ source: z.string().trim().min(1).max(100).nullable().optional().describe("Correct the lead's first-touch source (normally first-write-wins and set at creation — update only to FIX bad attribution). Pass null to clear."),
74
+ masterStatus: z.string().trim().min(1).max(40).optional().describe("Lifecycle status of the LEAD (master record), distinct from any deal's status: 'open' (default), 'won', 'lost', 'abandoned', 'disqualified', or an account-defined custom status. A terminal status (won/lost/abandoned/disqualified) SILENCES all outbound automations for this contact — drips, sequences, and scheduled sends stop. Use conduyt_get_lifecycle_settings to see the account's configured statuses."),
72
75
  customFields: z.record(z.any()).optional().describe("Structured custom-field values to merge, keyed by field key. Omit a key to leave it unchanged."),
73
76
  custom_fields: z.record(z.any()).optional().describe("Alias for customFields (snake_case). Provide only one of customFields / custom_fields."),
74
77
  }, async ({ id, customFields, custom_fields, ...updates }) => {
@@ -84,6 +84,7 @@ export function registerDealTools(server, client) {
84
84
  probability: z.number().min(0).max(1).optional().describe("Win probability as a 0–1 fraction (e.g. 0.75 = 75%)"),
85
85
  expectedCloseDate: z.string().optional().describe("Expected close date (ISO 8601, e.g. '2026-09-30')"),
86
86
  assignedTo: z.string().optional().describe("Assigned user (owner) UUID"),
87
+ source: z.string().trim().min(1).max(500).optional().describe("Attribution source for THIS deal — the trigger that produced it (e.g. 'sms-reply', 'booking:demo', 'partner-referral'). Each deal carries its own source; the contact's source stays the first-touch acquisition truth. 1-500 chars."),
87
88
  customFields: z.record(z.any()).optional().describe("Structured custom-field values keyed by field key, e.g. { limit_of_indemnity: 5000000, commission_pct: 12.5 }. Define fields first with conduyt_create_custom_field."),
88
89
  }, async (params) => {
89
90
  const result = await client.post("/api/v1/deals", params);
@@ -102,6 +103,7 @@ export function registerDealTools(server, client) {
102
103
  probability: z.number().min(0).max(1).optional().describe("Win probability as a 0–1 fraction (e.g. 0.75 = 75%)"),
103
104
  expectedCloseDate: z.string().optional().describe("Expected close date (ISO 8601)"),
104
105
  assignedTo: z.string().optional().describe("Reassign to user (owner) UUID"),
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."),
105
107
  customFields: z.record(z.any()).optional().describe("Structured custom-field values to merge, keyed by field key"),
106
108
  }, async ({ id, ...updates }) => {
107
109
  const result = await client.patch(`/api/v1/deals/${id}`, updates);
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ConduytClient } from "../client.js";
3
+ export declare function registerLifecycleTools(server: McpServer, client: ConduytClient): void;
@@ -0,0 +1,64 @@
1
+ import { z } from "zod";
2
+ import { formatResult } from "../client.js";
3
+ /**
4
+ * Lifecycle v2 settings (master lead status + intake deals). The master lead
5
+ * is the acquisition truth (first-write-wins source, one lifecycle status);
6
+ * deals are per-trigger children with their own source and owner. A terminal
7
+ * master status silences every outbound automation for that contact.
8
+ */
9
+ const DEFAULT_MASTER_STATUSES = ["open", "won", "lost", "abandoned", "disqualified"];
10
+ // Mirror of the API's masterStatuses validator (20 names max, 1-40 chars,
11
+ // letters/numbers/spaces/-/_ only) so agent-generated input fails fast here
12
+ // instead of late at the API.
13
+ const masterStatusName = z
14
+ .string()
15
+ .trim()
16
+ .min(1)
17
+ .max(40)
18
+ .regex(/^[a-z0-9][a-z0-9 _-]*$/i, "letters, numbers, spaces, - and _ only");
19
+ export function registerLifecycleTools(server, client) {
20
+ server.tool("conduyt_get_lifecycle_settings", "Read the account's Lifecycle v2 configuration, shaped for read-before-write: masterStatuses.effective is the full list a contact's masterStatus may take (the five built-in defaults plus the account's customs — the raw settings key stores only the customs); intakeDeals is the automatic deal-on-intake config, or null with an intakeDealsNote explaining that null can mean either not-configured or not-visible-to-your-key (the setting is admin-visible; member-tier responses omit it).", {}, async () => {
21
+ const result = await client.get("/api/v1/settings");
22
+ const raw = result?.data ?? {};
23
+ const customs = Array.isArray(raw.masterStatuses)
24
+ ? raw.masterStatuses.filter((s) => typeof s === "string")
25
+ : [];
26
+ // intakeDeals is admin-visible only: member-shaped responses omit it,
27
+ // and an unconfigured account also lacks it. The two are not
28
+ // distinguishable from here, so null is reported with a note saying so.
29
+ const intakeDeals = raw.intakeDeals && typeof raw.intakeDeals === "object" ? raw.intakeDeals : null;
30
+ const shaped = {
31
+ masterStatuses: {
32
+ defaults: DEFAULT_MASTER_STATUSES,
33
+ customs,
34
+ effective: [...DEFAULT_MASTER_STATUSES, ...customs.filter((c) => !DEFAULT_MASTER_STATUSES.includes(c.toLowerCase()))],
35
+ },
36
+ intakeDeals: intakeDeals ?? null,
37
+ intakeDealsNote: intakeDeals === null
38
+ ? "null = not configured, OR your key's role cannot read this admin-visible setting; configure/verify with an admin-tier key"
39
+ : undefined,
40
+ };
41
+ return formatResult({ data: shaped });
42
+ });
43
+ server.tool("conduyt_update_intake_deals", "Configure automatic deal creation on LIVE intake (inbound webhook contact upserts, public form submissions, public bookings). When enabled, each ELIGIBLE intake event mints a deal in the chosen pipeline/stage stamped with the trigger's own source. Exclusions that apply even under reInbound 'always': bulk imports never mint, staff/calendar/dialer-created appointments never mint, an exact replay of the same occurrence never mints twice, a same-source event for the same contact+pipeline within 10 minutes is treated as a delivery retry and skipped, and a contact whose masterStatus is terminal (won/lost/abandoned/disqualified) gets no new intake deals. reInbound governs contacts that already exist: 'if_no_open' (default, recommended) also skips when the contact already has an open deal in that pipeline; 'never' mints only for brand-new contacts. Requires settings:edit.", {
44
+ enabled: z.boolean().describe("Turn automatic intake-deal creation on or off"),
45
+ pipelineId: z.string().uuid().describe("Pipeline UUID the intake deals land in (conduyt_list_pipelines)"),
46
+ stageId: z.string().uuid().describe("Starting stage UUID within that pipeline"),
47
+ reInbound: z
48
+ .enum(["always", "if_no_open", "never"])
49
+ .optional()
50
+ .describe("Policy for contacts that already exist (default 'if_no_open')"),
51
+ }, async (params) => {
52
+ const result = await client.patch("/api/v1/settings", { intakeDeals: params });
53
+ return formatResult(result);
54
+ });
55
+ server.tool("conduyt_update_master_statuses", "Set the account's custom lead lifecycle statuses. The five defaults (open, won, lost, abandoned, disqualified) always exist and cannot be removed; this REPLACES the custom list on top of them (max 20 names, 1-40 chars each, letters/numbers/spaces/-/_ only). Terminal statuses stop all outbound automation for a contact. Requires settings:edit.", {
56
+ statuses: z
57
+ .array(masterStatusName)
58
+ .max(20)
59
+ .describe("Custom status names to make available alongside the defaults, e.g. ['nurturing', 'in underwriting']"),
60
+ }, async ({ statuses }) => {
61
+ const result = await client.patch("/api/v1/settings", { masterStatuses: statuses });
62
+ return formatResult(result);
63
+ });
64
+ }
@@ -0,0 +1,29 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ConduytClient } from "../client.js";
3
+ /**
4
+ * Reply capture: the dedicated receiving subdomain that lets replies to CRM
5
+ * emails record and thread against the contact.
6
+ *
7
+ * Lifecycle mirrors the sending domain: create -> DNS records -> verify ->
8
+ * live. It is a strict subdomain of the account's own VERIFIED sending
9
+ * domain (the API refuses anything else — that rule is the cross-tenant
10
+ * fence, not a formatting preference), and only ONE capture domain can be
11
+ * set up at a time.
12
+ *
13
+ * Outcomes that are NORMAL here, not failures to retry blindly:
14
+ * - 409 "already configured" — remove the current one first.
15
+ * - 409 "contact support to reconcile" — the row is in an operator-repair
16
+ * state (a provider create whose response was lost, or a stored provider
17
+ * id that does not resolve to this domain). Support repairs it through a
18
+ * super-admin, session-only endpoint that API keys deliberately cannot
19
+ * reach; do not loop on it.
20
+ * - 429 — the per-account budget (setup/teardown are limited per hour,
21
+ * verification per minute). DNS takes minutes to propagate; wait.
22
+ * - 503 — the shared email-provider allowance is busy with customer sends.
23
+ * Retry in a moment; it says nothing about the domain's health.
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.
28
+ */
29
+ export declare function registerReplyCaptureTools(server: McpServer, client: ConduytClient): void;
@@ -0,0 +1,63 @@
1
+ import { z } from "zod";
2
+ import { formatResult } from "../client.js";
3
+ /**
4
+ * Reply capture: the dedicated receiving subdomain that lets replies to CRM
5
+ * emails record and thread against the contact.
6
+ *
7
+ * Lifecycle mirrors the sending domain: create -> DNS records -> verify ->
8
+ * live. It is a strict subdomain of the account's own VERIFIED sending
9
+ * domain (the API refuses anything else — that rule is the cross-tenant
10
+ * fence, not a formatting preference), and only ONE capture domain can be
11
+ * set up at a time.
12
+ *
13
+ * Outcomes that are NORMAL here, not failures to retry blindly:
14
+ * - 409 "already configured" — remove the current one first.
15
+ * - 409 "contact support to reconcile" — the row is in an operator-repair
16
+ * state (a provider create whose response was lost, or a stored provider
17
+ * id that does not resolve to this domain). Support repairs it through a
18
+ * super-admin, session-only endpoint that API keys deliberately cannot
19
+ * reach; do not loop on it.
20
+ * - 429 — the per-account budget (setup/teardown are limited per hour,
21
+ * verification per minute). DNS takes minutes to propagate; wait.
22
+ * - 503 — the shared email-provider allowance is busy with customer sends.
23
+ * Retry in a moment; it says nothing about the domain's health.
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.
28
+ */
29
+ 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 () => {
31
+ const result = await client.get("/api/v1/email-domains/reply-capture");
32
+ return formatResult(result);
33
+ });
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.", {
35
+ domain: z
36
+ .string()
37
+ // TRIM BEFORE the emptiness check: the API trims and treats a blank
38
+ // as "omitted", silently creating reply.<sending-domain>. An empty
39
+ // agent variable would then burn mutation quota, take the account's
40
+ // ONE capture slot, and block sending-domain removal. Fail here.
41
+ .trim()
42
+ .min(1, "domain must not be blank — omit it entirely to use the suggested default")
43
+ .optional()
44
+ .describe("Capture subdomain, e.g. 'reply.acme.com'. Must be a subdomain of the account's verified sending domain. Omit to use the suggested default."),
45
+ }, async ({ domain }) => {
46
+ // An absent domain is serialized away, which is exactly how the API
47
+ // reads "use the suggested reply.<sending-domain>".
48
+ const result = await client.post("/api/v1/email-domains/reply-capture", { domain });
49
+ return formatResult(result);
50
+ });
51
+ server.tool("conduyt_reply_capture_verify", "Check the pending reply-capture domain's DNS and promote it to live once the provider reports it verified with inbound receiving in place. Returns verified=false plus the current DNS record states while records are still propagating — that is expected, not an error; poll sparingly (DNS takes minutes, and this endpoint is budgeted per account). Promotion is what turns on inbound matching and the Reply-To on outgoing email.", {}, async () => {
52
+ const result = await client.post("/api/v1/email-domains/reply-capture/verify", {});
53
+ return formatResult(result);
54
+ });
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.", {
56
+ confirm: z
57
+ .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."),
59
+ }, async () => {
60
+ const result = await client.del("/api/v1/email-domains/reply-capture");
61
+ return formatResult(result);
62
+ });
63
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt-mcp",
3
- "version": "4.14.0",
3
+ "version": "4.17.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",
@@ -203,7 +203,7 @@
203
203
  {
204
204
  "match": "/api/v1/email-domains",
205
205
  "status": "planned",
206
- "reason": "sending-domain management"
206
+ "reason": "sending-domain management (reply-capture subpaths ARE covered \u2014 see the reply_capture_* tools)"
207
207
  },
208
208
  {
209
209
  "match": "/api/v1/mailbox",
@@ -501,4 +501,4 @@
501
501
  "reason": "outbound webhook subscription management (distinct from the inbound /webhooks/ receivers)"
502
502
  }
503
503
  ]
504
- }
504
+ }
@@ -1,10 +1,10 @@
1
1
  {
2
- "generatedAt": "2026-07-23T16:00:26.290Z",
3
- "toolCount": 165,
4
- "totalEndpoints": 827,
2
+ "generatedAt": "2026-08-06T22:46:04.536Z",
3
+ "toolCount": 173,
4
+ "totalEndpoints": 842,
5
5
  "counts": {
6
- "covered": 154,
7
- "excluded": 240,
6
+ "covered": 161,
7
+ "excluded": 248,
8
8
  "planned": 433,
9
9
  "uncategorized": 0
10
10
  },
@@ -297,6 +297,14 @@
297
297
  "status": "excluded",
298
298
  "reason": "super-admin platform operations (Conduyt staff only)"
299
299
  },
300
+ "GET /api/v1/admin/reply-capture/reconcile": {
301
+ "status": "excluded",
302
+ "reason": "super-admin platform operations (Conduyt staff only)"
303
+ },
304
+ "POST /api/v1/admin/reply-capture/reconcile": {
305
+ "status": "excluded",
306
+ "reason": "super-admin platform operations (Conduyt staff only)"
307
+ },
300
308
  "GET /api/v1/admin/scale-readiness": {
301
309
  "status": "excluded",
302
310
  "reason": "super-admin platform operations (Conduyt staff only)"
@@ -1289,6 +1297,10 @@
1289
1297
  "status": "planned",
1290
1298
  "reason": "conversation thread surfaces"
1291
1299
  },
1300
+ "POST /api/v1/conversations/:contactId/compliance": {
1301
+ "status": "planned",
1302
+ "reason": "conversation thread surfaces"
1303
+ },
1292
1304
  "GET /api/v1/conversations/:contactId/state": {
1293
1305
  "status": "planned",
1294
1306
  "reason": "conversation thread surfaces"
@@ -1321,6 +1333,10 @@
1321
1333
  "status": "excluded",
1322
1334
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1323
1335
  },
1336
+ "GET /api/v1/cron/capture-rfc-ids": {
1337
+ "status": "excluded",
1338
+ "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1339
+ },
1324
1340
  "GET /api/v1/cron/cleanup-audit-logs": {
1325
1341
  "status": "excluded",
1326
1342
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
@@ -1373,6 +1389,10 @@
1373
1389
  "status": "excluded",
1374
1390
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1375
1391
  },
1392
+ "GET /api/v1/cron/process-consent-outbox": {
1393
+ "status": "excluded",
1394
+ "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1395
+ },
1376
1396
  "GET /api/v1/cron/process-data-model-jobs": {
1377
1397
  "status": "excluded",
1378
1398
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
@@ -1465,6 +1485,10 @@
1465
1485
  "status": "excluded",
1466
1486
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1467
1487
  },
1488
+ "GET /api/v1/cron/reap-qa-tenants": {
1489
+ "status": "excluded",
1490
+ "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1491
+ },
1468
1492
  "GET /api/v1/cron/reconcile-document-sends": {
1469
1493
  "status": "excluded",
1470
1494
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
@@ -1473,6 +1497,10 @@
1473
1497
  "status": "excluded",
1474
1498
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1475
1499
  },
1500
+ "GET /api/v1/cron/reconcile-reply-capture": {
1501
+ "status": "excluded",
1502
+ "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1503
+ },
1476
1504
  "GET /api/v1/cron/recurring-tasks": {
1477
1505
  "status": "excluded",
1478
1506
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
@@ -1533,6 +1561,14 @@
1533
1561
  "status": "excluded",
1534
1562
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1535
1563
  },
1564
+ "GET /api/v1/cron/sync-local-presence": {
1565
+ "status": "excluded",
1566
+ "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1567
+ },
1568
+ "POST /api/v1/cron/sync-local-presence": {
1569
+ "status": "excluded",
1570
+ "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1571
+ },
1536
1572
  "GET /api/v1/cron/task-reminders": {
1537
1573
  "status": "excluded",
1538
1574
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
@@ -1827,6 +1863,12 @@
1827
1863
  "status": "excluded",
1828
1864
  "reason": "realtime browser dialer (WebRTC tokens, live call control) — needs a live audio session"
1829
1865
  },
1866
+ "POST /api/v1/dialer/local-presence/sync": {
1867
+ "status": "covered",
1868
+ "tools": [
1869
+ "conduyt_sync_local_presence"
1870
+ ]
1871
+ },
1830
1872
  "POST /api/v1/dialer/mirror-token": {
1831
1873
  "status": "excluded",
1832
1874
  "reason": "realtime browser dialer (WebRTC tokens, live call control) — needs a live audio session"
@@ -2015,27 +2057,51 @@
2015
2057
  },
2016
2058
  "GET /api/v1/email-domains": {
2017
2059
  "status": "planned",
2018
- "reason": "sending-domain management"
2060
+ "reason": "sending-domain management (reply-capture subpaths ARE covered — see the reply_capture_* tools)"
2019
2061
  },
2020
2062
  "POST /api/v1/email-domains": {
2021
2063
  "status": "planned",
2022
- "reason": "sending-domain management"
2064
+ "reason": "sending-domain management (reply-capture subpaths ARE covered — see the reply_capture_* tools)"
2023
2065
  },
2024
2066
  "PATCH /api/v1/email-domains": {
2025
2067
  "status": "planned",
2026
- "reason": "sending-domain management"
2068
+ "reason": "sending-domain management (reply-capture subpaths ARE covered — see the reply_capture_* tools)"
2027
2069
  },
2028
2070
  "DELETE /api/v1/email-domains": {
2029
2071
  "status": "planned",
2030
- "reason": "sending-domain management"
2072
+ "reason": "sending-domain management (reply-capture subpaths ARE covered — see the reply_capture_* tools)"
2073
+ },
2074
+ "GET /api/v1/email-domains/reply-capture": {
2075
+ "status": "covered",
2076
+ "tools": [
2077
+ "conduyt_reply_capture_get"
2078
+ ]
2079
+ },
2080
+ "POST /api/v1/email-domains/reply-capture": {
2081
+ "status": "covered",
2082
+ "tools": [
2083
+ "conduyt_reply_capture_setup"
2084
+ ]
2085
+ },
2086
+ "DELETE /api/v1/email-domains/reply-capture": {
2087
+ "status": "covered",
2088
+ "tools": [
2089
+ "conduyt_reply_capture_remove"
2090
+ ]
2091
+ },
2092
+ "POST /api/v1/email-domains/reply-capture/verify": {
2093
+ "status": "covered",
2094
+ "tools": [
2095
+ "conduyt_reply_capture_verify"
2096
+ ]
2031
2097
  },
2032
2098
  "GET /api/v1/email-domains/status": {
2033
2099
  "status": "planned",
2034
- "reason": "sending-domain management"
2100
+ "reason": "sending-domain management (reply-capture subpaths ARE covered — see the reply_capture_* tools)"
2035
2101
  },
2036
2102
  "POST /api/v1/email-domains/verify": {
2037
2103
  "status": "planned",
2038
- "reason": "sending-domain management"
2104
+ "reason": "sending-domain management (reply-capture subpaths ARE covered — see the reply_capture_* tools)"
2039
2105
  },
2040
2106
  "POST /api/v1/email/send": {
2041
2107
  "status": "covered",
@@ -2437,6 +2503,10 @@
2437
2503
  "status": "planned",
2438
2504
  "reason": "message surfaces beyond send tools"
2439
2505
  },
2506
+ "GET /api/v1/messages/:id/media/:mediaIndex": {
2507
+ "status": "planned",
2508
+ "reason": "message surfaces beyond send tools"
2509
+ },
2440
2510
  "GET /api/v1/messages/sms/:id": {
2441
2511
  "status": "planned",
2442
2512
  "reason": "message surfaces beyond send tools"
@@ -2984,12 +3054,17 @@
2984
3054
  ]
2985
3055
  },
2986
3056
  "GET /api/v1/settings": {
2987
- "status": "planned",
2988
- "reason": "account settings read/update surfaces"
3057
+ "status": "covered",
3058
+ "tools": [
3059
+ "conduyt_get_lifecycle_settings"
3060
+ ]
2989
3061
  },
2990
3062
  "PATCH /api/v1/settings": {
2991
- "status": "planned",
2992
- "reason": "account settings read/update surfaces"
3063
+ "status": "covered",
3064
+ "tools": [
3065
+ "conduyt_update_intake_deals",
3066
+ "conduyt_update_master_statuses"
3067
+ ]
2993
3068
  },
2994
3069
  "GET /api/v1/settings/ai": {
2995
3070
  "status": "planned",