conduyt-mcp 4.50.0 → 4.52.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
@@ -86,7 +86,8 @@ MCP server for [Conduyt CRM](https://conduyt.app) — expose your CRM as AI-acce
86
86
  ### Messaging
87
87
  | Tool | Description |
88
88
  |------|-------------|
89
- | `conduyt_sms_transports` | What a workflow Send SMS step can speak through today: Twilio numbers, Project Blue lines held by active reps, the WhatsApp sender, webhook provider count, FromK.ai coming soon |
89
+ | `conduyt_sms_transports` | What a workflow Send SMS step can speak through today: Twilio numbers, Project Blue lines held by active reps, the WhatsApp sender, the Voicemail Drop recordings, webhook provider count, FromK.ai coming soon |
90
+ | `conduyt_voice_drop` | Place a voice drop to a contact: an outbound Twilio call with answering-machine detection that plays a Voicemail Drop recording to a machine (hangs up on a person unless onHuman is play); governed like a call (calls-tier consent, calling hours, one live call per lead) |
90
91
  | `conduyt_whatsapp_templates` | The account's WhatsApp templates (Twilio Content) with Meta's approval verdict: sid, text with {{n}} placeholders, variables, approval |
91
92
  | `conduyt_send_sms` | Send SMS to a contact (max 1600 chars); transport twilio, project_blue, or whatsapp (an approved template by Content SID, or free-form within the 24-hour window) |
92
93
  | `conduyt_send_email` | Send a single email to a contact |
package/dist/index.js CHANGED
@@ -42,6 +42,7 @@ import { registerReplyCaptureTools } from "./tools/reply-capture.js";
42
42
  import { registerCallFlowTools } from "./tools/call-flows.js";
43
43
  import { registerProjectBlueTools } from "./tools/project-blue.js";
44
44
  import { registerWhatsappTools } from "./tools/whatsapp.js";
45
+ import { registerFromkTools } from "./tools/fromk.js";
45
46
  import { registerReportTools } from "./tools/reports.js";
46
47
  import { registerReportDashboardTools } from "./tools/dashboards.js";
47
48
  const apiUrl = process.env.CONDUYT_API_URL;
@@ -108,6 +109,7 @@ registerReplyCaptureTools(server, client);
108
109
  registerCallFlowTools(server, client);
109
110
  registerProjectBlueTools(server, client);
110
111
  registerWhatsappTools(server, client);
112
+ registerFromkTools(server, client);
111
113
  registerReportTools(server, client);
112
114
  registerReportDashboardTools(server, client);
113
115
  const transport = new StdioServerTransport();
@@ -0,0 +1,10 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ConduytClient } from "../client.js";
3
+ /**
4
+ * FromK.ai (2026-09-14): Conduyt's own texting platform as a native SMS transport. The tenant's FromK.ai workspace
5
+ * texts (its lines, its credits, its consent and STOP rules); Conduyt drives it with the workspace's API key. These
6
+ * tools cover what an AI operator needs: is it connected, which lines are send-ready (the fromkPhoneNumberId a step or
7
+ * conduyt_send_sms picks), refreshing that list, connecting or replacing the key, storing the webhook signing secret,
8
+ * and disconnecting.
9
+ */
10
+ export declare function registerFromkTools(server: McpServer, client: ConduytClient): void;
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+ import { formatResult } from "../client.js";
3
+ /**
4
+ * FromK.ai (2026-09-14): Conduyt's own texting platform as a native SMS transport. The tenant's FromK.ai workspace
5
+ * texts (its lines, its credits, its consent and STOP rules); Conduyt drives it with the workspace's API key. These
6
+ * tools cover what an AI operator needs: is it connected, which lines are send-ready (the fromkPhoneNumberId a step or
7
+ * conduyt_send_sms picks), refreshing that list, connecting or replacing the key, storing the webhook signing secret,
8
+ * and disconnecting.
9
+ */
10
+ export function registerFromkTools(server, client) {
11
+ server.tool("conduyt_fromk_status", "FromK.ai connection for this account (GET /api/v1/settings/fromk): status connected | not_configured, the API key masked, the lines snapshot (phoneNumberId, phoneE164, status, provider, sendReady) and the webhook setup (the URL the tenant pastes into FromK.ai → Developers → Webhooks, whether the signing secret is stored, the events to subscribe). Requires settings:view.", {}, async () => {
12
+ const result = await client.get("/api/v1/settings/fromk");
13
+ return formatResult(result);
14
+ });
15
+ server.tool("conduyt_fromk_lines", "Refresh the FromK.ai line list from the workspace with the stored key and store it as the new snapshot (GET /api/v1/settings/fromk/lines): every line with sendReady (assigned, ownership verified, SMS-capable, on a real carrier). A send-ready phoneNumberId is a valid send_sms config.fromkPhoneNumberId / conduyt_send_sms fromkPhoneNumberId. 409 fromk_not_configured, 422 key_revoked, 502 vendor_unavailable (the old snapshot is kept). Requires settings:view.", {}, async () => {
16
+ const result = await client.get("/api/v1/settings/fromk/lines");
17
+ return formatResult(result);
18
+ });
19
+ server.tool("conduyt_fromk_connect", "Connect or replace the account's FromK.ai workspace API key (PUT /api/v1/settings/fromk): fk_… with numbers:read, contacts:write and messages:send. Verified with FromK.ai before it is stored (it must list the workspace's lines: 422 key_rejected / key_scope, 502 vendor_unavailable, nothing stored). Pass disconnect: true to forget the key, the signing secret, the lines and every contact link instead (DELETE): workflow steps on the FromK.ai channel stop sending. Requires settings:edit.", {
20
+ apiKey: z.string().min(24).max(200).optional().describe("FromK.ai workspace API key (fk_…); required unless disconnect is true"),
21
+ disconnect: z.boolean().optional().describe("true = forget everything FromK.ai (DELETE)"),
22
+ }, async ({ apiKey, disconnect }) => {
23
+ if (disconnect) {
24
+ const result = await client.del("/api/v1/settings/fromk");
25
+ return formatResult(result);
26
+ }
27
+ if (!apiKey)
28
+ throw new Error("apiKey is required unless disconnect is true");
29
+ const result = await client.put("/api/v1/settings/fromk", { apiKey });
30
+ return formatResult(result);
31
+ });
32
+ server.tool("conduyt_fromk_webhook_secret", "Store (PUT /api/v1/settings/fromk/webhook) or forget (DELETE, clear: true) the signing secret of the FromK.ai webhook endpoint the tenant pointed at Conduyt (whsec_…, shown once by FromK.ai when the endpoint is created). Until it is stored, FromK.ai replies and delivery reports are refused. 409 fromk_not_configured before the key is connected. Requires settings:edit.", {
33
+ secret: z.string().regex(/^whsec_[A-Za-z0-9_-]{16,}$/).optional().describe("the endpoint's signing secret; required unless clear is true"),
34
+ clear: z.boolean().optional().describe("true = forget the stored secret (DELETE)"),
35
+ }, async ({ secret, clear }) => {
36
+ if (clear) {
37
+ const result = await client.del("/api/v1/settings/fromk/webhook");
38
+ return formatResult(result);
39
+ }
40
+ if (!secret)
41
+ throw new Error("secret is required unless clear is true");
42
+ const result = await client.put("/api/v1/settings/fromk/webhook", { secret });
43
+ return formatResult(result);
44
+ });
45
+ }
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { formatResult } from "../client.js";
3
3
  export function registerMessagingTools(server, client) {
4
- server.tool("conduyt_sms_transports", "What a workflow Send SMS step can speak through TODAY (GET /api/v1/sms/transports): twilio {connected, numbers[] = the account number + every active rep's assigned line, each a valid send_sms config.fromNumber}, projectBlue {connected, lines[] = Project Blue iPhone lines held by active reps, each lineId a valid config.projectBlueLineId for smsMode project_blue}, whatsapp {connected, sender = the account's WhatsApp sender for smsMode whatsapp; templates come from conduyt_whatsapp_templates}, webhook {providers = active webhook providers for smsMode webhook}, fromk {status: coming_soon}. Use it before building or editing a send_sms step so you never configure a transport that is not connected. Requires automations:view.", {}, async () => {
4
+ server.tool("conduyt_sms_transports", "What a workflow Send SMS step can speak through TODAY (GET /api/v1/sms/transports): twilio {connected, numbers[] = the account number + every active rep's assigned line, each a valid send_sms config.fromNumber}, projectBlue {connected, lines[] = Project Blue iPhone lines held by active reps, each lineId a valid config.projectBlueLineId for smsMode project_blue}, whatsapp {connected, sender = the account's WhatsApp sender for smsMode whatsapp; templates come from conduyt_whatsapp_templates}, voiceDrop {connected = Twilio is connected, recordings {hasDefault, templates[] = names valid as config.voiceDropRecording for smsMode voice_drop}}, webhook {providers = active webhook providers for smsMode webhook}, fromk {status connected | setup, connected, lines[] = the FromK.ai workspace's send-ready lines, each phoneNumberId a valid config.fromkPhoneNumberId for smsMode fromk}. Use it before building or editing a send_sms step so you never configure a transport that is not connected. Requires automations:view.", {}, async () => {
5
5
  const result = await client.get("/api/v1/sms/transports");
6
6
  return formatResult(result);
7
7
  });
@@ -9,11 +9,21 @@ export function registerMessagingTools(server, client) {
9
9
  const result = await client.get("/api/v1/sms/whatsapp/templates");
10
10
  return formatResult(result);
11
11
  });
12
+ server.tool("conduyt_voice_drop", "Place a voice drop to a contact (POST /api/v1/calls/voice-drop): an outbound Twilio call with answering-machine detection that plays one of the account's Voicemail Drop recordings (Settings → Twilio → Voicemail Drop; names from conduyt_sms_transports voiceDrop.recordings) when a machine answers, and hangs up quietly when a person answers unless onHuman is play. Governed like a call: calls-tier consent (a texting STOP does not block it, a DNC or litigator row does), the account's calling hours, one live call per lead. Placed from your own Twilio line (else the account number) unless fromNumber names a number the account owns. Returns 202 with the Call row; the outcome lands on it through the voice webhooks (disposition voicemail_drop = played to a machine, voice_drop_human = a person answered, voice_drop_live = played to a person, voice_drop_fax; status no-answer/busy when nobody picked up). Refusals carry a code (dnc, litigator, calling_window, active_call, missing_phone, voice_drop_recording_missing, voice_drop_not_configured); 429 and 502 are safe to retry. Requires dialer:create.", {
13
+ contactId: z.string().uuid().describe("Contact UUID — the drop calls the contact's primary number"),
14
+ recording: z.string().min(1).max(100).optional().describe("A Voicemail Drop template name (see conduyt_sms_transports voiceDrop.recordings.templates); omit for the default recording"),
15
+ fromNumber: z.string().regex(/^\+[1-9]\d{6,14}$/).optional().describe("Caller ID the account owns (the account number or an active rep's line, E.164); omit for your own line"),
16
+ onHuman: z.enum(["hang_up", "play"]).optional().describe("When a person answers: hang_up (default) ends the call quietly; play plays the recording anyway"),
17
+ }, async (params) => {
18
+ const result = await client.post("/api/v1/calls/voice-drop", params);
19
+ return formatResult(result);
20
+ });
12
21
  server.tool("conduyt_send_sms", "Send an SMS message to a contact. Contact must have a phone number and not have opted out. Max 1600 characters.", {
13
22
  contactId: z.string().describe("Contact UUID — must have a phone number on file"),
14
23
  body: z.string().max(1600).optional().describe("Message text (max 1600 characters). Required unless a WhatsApp template is sent (transport whatsapp + whatsappContentSid): then the template Twilio renders is the text, and a media-only template has none."),
15
24
  fromNumber: z.string().optional().describe("Sender phone number (defaults to account's primary number)"),
16
- transport: z.enum(["twilio", "project_blue", "whatsapp"]).optional().describe("'project_blue' sends from the acting user's own Project Blue iPhone line (iMessage when the lead is on Apple, SMS otherwise; the user must have a line assigned in Settings → Project Blue). 'whatsapp' sends from the account's WhatsApp sender (Settings → WhatsApp): free-form text only within 24 hours of the contact's last WhatsApp message (422 whatsapp_window_closed otherwise), or an approved template via whatsappContentSid. Default: Twilio."),
25
+ transport: z.enum(["twilio", "project_blue", "whatsapp", "fromk"]).optional().describe("'project_blue' sends from the acting user's own Project Blue iPhone line (iMessage when the lead is on Apple, SMS otherwise; the user must have a line assigned in Settings → Project Blue). 'whatsapp' sends from the account's WhatsApp sender (Settings → WhatsApp): free-form text only within 24 hours of the contact's last WhatsApp message (422 whatsapp_window_closed otherwise), or an approved template via whatsappContentSid. 'fromk' sends through the account's FromK.ai workspace (Settings → FromK.ai) from fromkPhoneNumberId, else the workspace's single send-ready line (422 fromk_line_required with several). Default: Twilio."),
26
+ fromkPhoneNumberId: z.string().uuid().optional().describe("transport fromk: the FromK.ai line to send from (a send-ready phoneNumberId from conduyt_fromk_status / conduyt_sms_transports fromk.lines); omit when the workspace has one send-ready line"),
17
27
  whatsappContentSid: z.string().regex(/^HX[0-9a-fA-F]{32}$/).optional().describe("transport whatsapp: an APPROVED WhatsApp template (Twilio Content SID from conduyt_whatsapp_templates); Conduyt reads the template from Twilio at send time, records its text with the variables in place, and refuses an unlisted (whatsapp_template_invalid) or unapproved (whatsapp_template_not_approved) one. body is still required but the template's text is what is sent and recorded."),
18
28
  whatsappContentVariables: z.record(z.string().regex(/^\d+$/), z.string().max(1024)).optional().describe("transport whatsapp with a template: placeholder number → value, e.g. { \"1\": \"Alex\" }"),
19
29
  idempotencyKey: z.string().min(8).max(200).optional().describe("Operation key for at-most-once sends: reuse the SAME key when retrying a send whose outcome you did not see; a settled key answers with the original message and never sends twice."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt-mcp",
3
- "version": "4.50.0",
3
+ "version": "4.52.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",
@@ -1,10 +1,10 @@
1
1
  {
2
- "generatedAt": "2026-09-11T21:39:51.508Z",
3
- "toolCount": 255,
4
- "totalEndpoints": 963,
2
+ "generatedAt": "2026-09-14T15:23:41.003Z",
3
+ "toolCount": 260,
4
+ "totalEndpoints": 972,
5
5
  "counts": {
6
- "covered": 238,
7
- "excluded": 284,
6
+ "covered": 245,
7
+ "excluded": 286,
8
8
  "planned": 441,
9
9
  "uncategorized": 0
10
10
  },
@@ -1154,6 +1154,12 @@
1154
1154
  "status": "planned",
1155
1155
  "reason": "call subroutes beyond list/log"
1156
1156
  },
1157
+ "POST /api/v1/calls/voice-drop": {
1158
+ "status": "covered",
1159
+ "tools": [
1160
+ "conduyt_voice_drop"
1161
+ ]
1162
+ },
1157
1163
  "GET /api/v1/chat/channels": {
1158
1164
  "status": "planned",
1159
1165
  "reason": "team chat read/post for agents"
@@ -3617,6 +3623,42 @@
3617
3623
  "status": "planned",
3618
3624
  "reason": "account settings read/update surfaces"
3619
3625
  },
3626
+ "GET /api/v1/settings/fromk": {
3627
+ "status": "covered",
3628
+ "tools": [
3629
+ "conduyt_fromk_status"
3630
+ ]
3631
+ },
3632
+ "PUT /api/v1/settings/fromk": {
3633
+ "status": "covered",
3634
+ "tools": [
3635
+ "conduyt_fromk_connect"
3636
+ ]
3637
+ },
3638
+ "DELETE /api/v1/settings/fromk": {
3639
+ "status": "covered",
3640
+ "tools": [
3641
+ "conduyt_fromk_connect"
3642
+ ]
3643
+ },
3644
+ "GET /api/v1/settings/fromk/lines": {
3645
+ "status": "covered",
3646
+ "tools": [
3647
+ "conduyt_fromk_lines"
3648
+ ]
3649
+ },
3650
+ "PUT /api/v1/settings/fromk/webhook": {
3651
+ "status": "covered",
3652
+ "tools": [
3653
+ "conduyt_fromk_webhook_secret"
3654
+ ]
3655
+ },
3656
+ "DELETE /api/v1/settings/fromk/webhook": {
3657
+ "status": "covered",
3658
+ "tools": [
3659
+ "conduyt_fromk_webhook_secret"
3660
+ ]
3661
+ },
3620
3662
  "POST /api/v1/settings/integrations/:id/test": {
3621
3663
  "status": "planned",
3622
3664
  "reason": "account settings read/update surfaces"
@@ -4240,6 +4282,10 @@
4240
4282
  "status": "planned",
4241
4283
  "reason": "outbound webhook delivery replay — agent-relevant"
4242
4284
  },
4285
+ "POST /api/v1/webhooks/sms/fromk/:accountId": {
4286
+ "status": "excluded",
4287
+ "reason": "inbound provider callback receivers (Twilio/Resend/etc.), not client-callable"
4288
+ },
4243
4289
  "POST /api/v1/webhooks/sms/inbound": {
4244
4290
  "status": "excluded",
4245
4291
  "reason": "inbound provider callback receivers (Twilio/Resend/etc.), not client-callable"
@@ -4300,6 +4346,10 @@
4300
4346
  "status": "excluded",
4301
4347
  "reason": "inbound provider callback receivers (Twilio/Resend/etc.), not client-callable"
4302
4348
  },
4349
+ "POST /api/v1/webhooks/voice/voice-drop": {
4350
+ "status": "excluded",
4351
+ "reason": "inbound provider callback receivers (Twilio/Resend/etc.), not client-callable"
4352
+ },
4303
4353
  "POST /api/v1/webhooks/voice/voicemail": {
4304
4354
  "status": "excluded",
4305
4355
  "reason": "inbound provider callback receivers (Twilio/Resend/etc.), not client-callable"