conduyt-mcp 4.14.0 → 4.15.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
+ ## 169 Tools
6
6
 
7
7
  ### Discovery
8
8
  | Tool | Description |
@@ -258,6 +258,18 @@ 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
+
261
273
  ## Setup
262
274
 
263
275
  ```bash
package/dist/index.js CHANGED
@@ -36,6 +36,7 @@ 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";
39
40
  const apiUrl = process.env.CONDUYT_API_URL;
40
41
  const apiKey = process.env.CONDUYT_API_KEY;
41
42
  if (!apiUrl || !apiKey) {
@@ -94,5 +95,6 @@ registerAiExtendedTools(server, client);
94
95
  registerWorkflowSandboxTools(server, client);
95
96
  registerPrivacyTools(server, client);
96
97
  registerAccountExportTools(server, client);
98
+ registerLifecycleTools(server, client);
97
99
  const transport = new StdioServerTransport();
98
100
  await server.connect(transport);
@@ -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
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "conduyt-mcp",
3
- "version": "4.14.0",
4
- "description": "MCP server for Conduyt CRM expose CRM operations as AI-accessible tools",
3
+ "version": "4.15.0",
4
+ "description": "MCP server for Conduyt CRM \u2014 expose CRM operations as AI-accessible tools",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -57,4 +57,4 @@
57
57
  "tsx": "^4.19.0",
58
58
  "@types/node": "^22.0.0"
59
59
  }
60
- }
60
+ }
@@ -1,11 +1,11 @@
1
1
  {
2
- "generatedAt": "2026-07-23T16:00:26.290Z",
3
- "toolCount": 165,
4
- "totalEndpoints": 827,
2
+ "generatedAt": "2026-07-29T16:12:55.014Z",
3
+ "toolCount": 169,
4
+ "totalEndpoints": 831,
5
5
  "counts": {
6
- "covered": 154,
7
- "excluded": 240,
8
- "planned": 433,
6
+ "covered": 157,
7
+ "excluded": 243,
8
+ "planned": 431,
9
9
  "uncategorized": 0
10
10
  },
11
11
  "matrix": {
@@ -1465,6 +1465,10 @@
1465
1465
  "status": "excluded",
1466
1466
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1467
1467
  },
1468
+ "GET /api/v1/cron/reap-qa-tenants": {
1469
+ "status": "excluded",
1470
+ "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1471
+ },
1468
1472
  "GET /api/v1/cron/reconcile-document-sends": {
1469
1473
  "status": "excluded",
1470
1474
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
@@ -1533,6 +1537,14 @@
1533
1537
  "status": "excluded",
1534
1538
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1535
1539
  },
1540
+ "GET /api/v1/cron/sync-local-presence": {
1541
+ "status": "excluded",
1542
+ "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1543
+ },
1544
+ "POST /api/v1/cron/sync-local-presence": {
1545
+ "status": "excluded",
1546
+ "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1547
+ },
1536
1548
  "GET /api/v1/cron/task-reminders": {
1537
1549
  "status": "excluded",
1538
1550
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
@@ -1827,6 +1839,12 @@
1827
1839
  "status": "excluded",
1828
1840
  "reason": "realtime browser dialer (WebRTC tokens, live call control) — needs a live audio session"
1829
1841
  },
1842
+ "POST /api/v1/dialer/local-presence/sync": {
1843
+ "status": "covered",
1844
+ "tools": [
1845
+ "conduyt_sync_local_presence"
1846
+ ]
1847
+ },
1830
1848
  "POST /api/v1/dialer/mirror-token": {
1831
1849
  "status": "excluded",
1832
1850
  "reason": "realtime browser dialer (WebRTC tokens, live call control) — needs a live audio session"
@@ -2984,12 +3002,17 @@
2984
3002
  ]
2985
3003
  },
2986
3004
  "GET /api/v1/settings": {
2987
- "status": "planned",
2988
- "reason": "account settings read/update surfaces"
3005
+ "status": "covered",
3006
+ "tools": [
3007
+ "conduyt_get_lifecycle_settings"
3008
+ ]
2989
3009
  },
2990
3010
  "PATCH /api/v1/settings": {
2991
- "status": "planned",
2992
- "reason": "account settings read/update surfaces"
3011
+ "status": "covered",
3012
+ "tools": [
3013
+ "conduyt_update_intake_deals",
3014
+ "conduyt_update_master_statuses"
3015
+ ]
2993
3016
  },
2994
3017
  "GET /api/v1/settings/ai": {
2995
3018
  "status": "planned",