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 +13 -1
- package/dist/index.js +2 -0
- package/dist/tools/calls.js +4 -0
- package/dist/tools/contacts.js +4 -1
- package/dist/tools/deals.js +2 -0
- package/dist/tools/lifecycle.d.ts +3 -0
- package/dist/tools/lifecycle.js +64 -0
- package/package.json +3 -3
- package/scripts/coverage-matrix.json +33 -10
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
|
-
##
|
|
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);
|
package/dist/tools/calls.js
CHANGED
|
@@ -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
|
}
|
package/dist/tools/contacts.js
CHANGED
|
@@ -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 }) => {
|
package/dist/tools/deals.js
CHANGED
|
@@ -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,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.
|
|
4
|
-
"description": "MCP server for Conduyt CRM
|
|
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-
|
|
3
|
-
"toolCount":
|
|
4
|
-
"totalEndpoints":
|
|
2
|
+
"generatedAt": "2026-07-29T16:12:55.014Z",
|
|
3
|
+
"toolCount": 169,
|
|
4
|
+
"totalEndpoints": 831,
|
|
5
5
|
"counts": {
|
|
6
|
-
"covered":
|
|
7
|
-
"excluded":
|
|
8
|
-
"planned":
|
|
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": "
|
|
2988
|
-
"
|
|
3005
|
+
"status": "covered",
|
|
3006
|
+
"tools": [
|
|
3007
|
+
"conduyt_get_lifecycle_settings"
|
|
3008
|
+
]
|
|
2989
3009
|
},
|
|
2990
3010
|
"PATCH /api/v1/settings": {
|
|
2991
|
-
"status": "
|
|
2992
|
-
"
|
|
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",
|