conduyt-mcp 4.20.0 → 4.21.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.d.ts CHANGED
@@ -60,3 +60,13 @@ export declare function formatResult(data: unknown): {
60
60
  text: string;
61
61
  }>;
62
62
  };
63
+ /** A tool-level failure (preflight refusals, guard violations): MCP treats a
64
+ * result without isError as SUCCESS, so refusals must set it explicitly or
65
+ * clients proceed as if the call happened. */
66
+ export declare function formatToolError(message: string): {
67
+ content: Array<{
68
+ type: "text";
69
+ text: string;
70
+ }>;
71
+ isError: true;
72
+ };
package/dist/client.js CHANGED
@@ -253,3 +253,12 @@ export function formatResult(data) {
253
253
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
254
254
  };
255
255
  }
256
+ /** A tool-level failure (preflight refusals, guard violations): MCP treats a
257
+ * result without isError as SUCCESS, so refusals must set it explicitly or
258
+ * clients proceed as if the call happened. */
259
+ export function formatToolError(message) {
260
+ return {
261
+ content: [{ type: "text", text: JSON.stringify({ ok: false, error: message }, null, 2) }],
262
+ isError: true,
263
+ };
264
+ }
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ import { registerPrivacyTools } from "./tools/privacy.js";
38
38
  import { registerAccountExportTools } from "./tools/account-exports.js";
39
39
  import { registerLifecycleTools } from "./tools/lifecycle.js";
40
40
  import { registerReplyCaptureTools } from "./tools/reply-capture.js";
41
+ import { registerCallFlowTools } from "./tools/call-flows.js";
41
42
  const apiUrl = process.env.CONDUYT_API_URL;
42
43
  const apiKey = process.env.CONDUYT_API_KEY;
43
44
  if (!apiUrl || !apiKey) {
@@ -98,5 +99,6 @@ registerPrivacyTools(server, client);
98
99
  registerAccountExportTools(server, client);
99
100
  registerLifecycleTools(server, client);
100
101
  registerReplyCaptureTools(server, client);
102
+ registerCallFlowTools(server, client);
101
103
  const transport = new StdioServerTransport();
102
104
  await server.connect(transport);
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ConduytClient } from "../client.js";
3
+ export declare function registerCallFlowTools(server: McpServer, client: ConduytClient): void;
@@ -0,0 +1,142 @@
1
+ import { z } from "zod";
2
+ import { formatResult, formatToolError } from "../client.js";
3
+ /**
4
+ * Call flows (#25 / B11) — the IVR graphs inbound numbers execute — and
5
+ * ring groups, their companion routing primitive. Both surfaces are
6
+ * "settings"-scoped at the API-key boundary: grant the key the settings
7
+ * scope or these tools return 403.
8
+ *
9
+ * The flow graph (draftGraph) is a JSON node/edge document; the API is the
10
+ * single validator (POST /call-flows/:id/validate previews problems, and
11
+ * publish refuses an invalid graph), so the tools pass it through untyped
12
+ * and surface the API's structured errors verbatim.
13
+ */
14
+ /** (p25-mcp-r3) An explicit graph must carry the parseFlowGraph minimum
15
+ * (string entryId + nodes array): the API silently substitutes a default
16
+ * entry→voicemail flow for anything unparseable, which a later publish
17
+ * would put live on the number. Passthrough keeps every nested field. */
18
+ const flowGraphSchema = z
19
+ .object({ entryId: z.string(), nodes: z.array(z.unknown()) })
20
+ .passthrough();
21
+ export function registerCallFlowTools(server, client) {
22
+ server.tool("conduyt_list_call_flows", "List call flows (IVR routing graphs) with their status (draft/published) and attached phone numbers.", {}, async () => {
23
+ const result = await client.get("/api/v1/call-flows");
24
+ return formatResult(result);
25
+ });
26
+ server.tool("conduyt_get_call_flow", "Get one call flow including its draft and published graphs.", {
27
+ id: z.string().describe("Call flow UUID"),
28
+ }, async ({ id }) => {
29
+ const result = await client.get(`/api/v1/call-flows/${id}`);
30
+ return formatResult(result);
31
+ });
32
+ server.tool("conduyt_create_call_flow", "Create a call flow. Starts as a DRAFT — edit the graph, validate, then publish to make it live on its number.", {
33
+ name: z.string().describe("Flow name"),
34
+ phoneNumber: z.string().optional().describe("E.164 number this flow answers (must belong to the account)"),
35
+ draftGraph: flowGraphSchema.optional().describe("Initial flow graph — MUST have a string entryId and a nodes array (anything else is silently replaced by a default voicemail flow server-side)"),
36
+ }, async (params) => {
37
+ const result = await client.post("/api/v1/call-flows", params);
38
+ return formatResult(result);
39
+ });
40
+ server.tool("conduyt_update_call_flow", "Update a call flow's DRAFT (name, number, graph). Published behavior does not change until the next publish. Replacing draftGraph REQUIRES expectedDraftRevision (the API 428s without it; a concurrent graph edit then 409s instead of being overwritten — the token is graph-specific, so live calls and publish never invalidate it).", {
41
+ id: z.string().describe("Call flow UUID"),
42
+ name: z.string().optional().describe("New name"),
43
+ phoneNumber: z.string().optional().describe("New E.164 number"),
44
+ draftGraph: flowGraphSchema.optional().describe("Replacement draft graph — MUST have a string entryId and a nodes array; REQUIRES expectedDraftRevision"),
45
+ expectedDraftRevision: z.number().int().min(0).optional().describe("draftRevision from your last conduyt_get_call_flow read (REQUIRED when replacing draftGraph)"),
46
+ }, async ({ id, ...updates }) => {
47
+ if (updates.draftGraph !== undefined && updates.expectedDraftRevision === undefined) {
48
+ return formatToolError("Replacing draftGraph requires expectedDraftRevision — call conduyt_get_call_flow first and pass its draftRevision.");
49
+ }
50
+ const result = await client.patch(`/api/v1/call-flows/${id}`, updates);
51
+ return formatResult(result);
52
+ });
53
+ server.tool("conduyt_delete_call_flow", "Delete a call flow. A PUBLISHED flow refuses deletion (409) — call conduyt_unpublish_call_flow first.", {
54
+ id: z.string().describe("Call flow UUID"),
55
+ }, async ({ id }) => {
56
+ const result = await client.del(`/api/v1/call-flows/${id}`);
57
+ return formatResult(result);
58
+ });
59
+ server.tool("conduyt_validate_call_flow", "Validate a call flow's draft graph WITHOUT publishing — returns structured problems (unreachable nodes, missing targets, etc.).", {
60
+ id: z.string().describe("Call flow UUID"),
61
+ }, async ({ id }) => {
62
+ const result = await client.get(`/api/v1/call-flows/${id}/validate`);
63
+ return formatResult(result);
64
+ });
65
+ server.tool("conduyt_publish_call_flow", "Publish a call flow's draft, making it live on its phone number. Refuses an invalid graph. expectedUpdatedAt is REQUIRED (the API returns 428 without it): read the flow first and pass its updatedAt — a concurrent edit then fails 409 instead of being published over.", {
66
+ id: z.string().describe("Call flow UUID"),
67
+ expectedUpdatedAt: z.string().describe("REQUIRED: updatedAt from your last conduyt_get_call_flow read (optimistic concurrency; missing → 428)"),
68
+ }, async ({ id, expectedUpdatedAt }) => {
69
+ const result = await client.post(`/api/v1/call-flows/${id}/publish`, { expectedUpdatedAt });
70
+ return formatResult(result);
71
+ });
72
+ server.tool("conduyt_unpublish_call_flow", "Unpublish a call flow — its number stops executing the graph (falls back to default routing).", {
73
+ id: z.string().describe("Call flow UUID"),
74
+ }, async ({ id }) => {
75
+ const result = await client.post(`/api/v1/call-flows/${id}/unpublish`, {});
76
+ return formatResult(result);
77
+ });
78
+ server.tool("conduyt_simulate_call_flow", "Answer \"if someone calls right now, who picks up?\" — simulates the DRAFT graph against LIVE member availability (DIDs, on-call state). No side effects.", {
79
+ id: z.string().describe("Call flow UUID"),
80
+ }, async ({ id }) => {
81
+ const result = await client.post(`/api/v1/call-flows/${id}/simulate`, {});
82
+ return formatResult(result);
83
+ });
84
+ server.tool("conduyt_call_flow_roster", "Get the routing roster: members with DIDs and their live availability — the people call-flow and ring-group steps can ring.", {}, async () => {
85
+ const result = await client.get("/api/v1/call-flows/roster");
86
+ return formatResult(result);
87
+ });
88
+ server.tool("conduyt_list_ring_groups", "List ring groups (simultaneous/sequential ring sets over member DIDs) with their numbers and members.", {}, async () => {
89
+ const result = await client.get("/api/v1/ring-groups");
90
+ return formatResult(result);
91
+ });
92
+ server.tool("conduyt_get_ring_group", "Get one ring group.", {
93
+ id: z.string().describe("Ring group UUID"),
94
+ }, async ({ id }) => {
95
+ const result = await client.get(`/api/v1/ring-groups/${id}`);
96
+ return formatResult(result);
97
+ });
98
+ server.tool("conduyt_create_ring_group", "Create a ring group on an account number. Without memberIds the group is created INACTIVE.", {
99
+ name: z.string().describe("Ring group name"),
100
+ phoneNumber: z.string().describe("REQUIRED: account E.164 number that rings this group"),
101
+ memberIds: z.array(z.string()).optional().describe("Member user UUIDs (omit → the group is created INACTIVE; memberless groups never ring)"),
102
+ strategy: z.string().optional().describe("Ring strategy (e.g. simultaneous, sequential)"),
103
+ ringSeconds: z.number().int().min(5).max(120).optional().describe("Seconds to ring before advancing/failing over (5-120)"),
104
+ isActive: z.boolean().optional().describe("Whether the group is active"),
105
+ }, async (params) => {
106
+ const result = await client.post("/api/v1/ring-groups", params);
107
+ return formatResult(result);
108
+ });
109
+ server.tool("conduyt_update_ring_group", "Update a ring group (name, number, members, strategy, ring time, active). Replacing memberIds REQUIRES expectedUpdatedAt (the API 428s without it); passing it on any update makes a concurrent edit 409 instead of being overwritten.", {
110
+ id: z.string().describe("Ring group UUID"),
111
+ name: z.string().optional().describe("New name"),
112
+ phoneNumber: z.string().optional().describe("New E.164 number"),
113
+ memberIds: z.array(z.string()).optional().describe("Replacement member user UUIDs — REQUIRES expectedUpdatedAt"),
114
+ strategy: z.string().optional().describe("Ring strategy"),
115
+ ringSeconds: z.number().int().min(5).max(120).optional().describe("Seconds to ring (5-120)"),
116
+ isActive: z.boolean().optional().describe("Active flag"),
117
+ expectedUpdatedAt: z.string().optional().describe("updatedAt from your last conduyt_get_ring_group read (REQUIRED when replacing memberIds)"),
118
+ }, async ({ id, ...updates }) => {
119
+ if (updates.memberIds !== undefined && updates.expectedUpdatedAt === undefined) {
120
+ // (p25-mcp-r2) isError REQUIRED: without it MCP reports success and
121
+ // the client proceeds although no PATCH occurred.
122
+ return formatToolError("Replacing memberIds requires expectedUpdatedAt — call conduyt_get_ring_group first and pass its updatedAt.");
123
+ }
124
+ const result = await client.patch(`/api/v1/ring-groups/${id}`, updates);
125
+ return formatResult(result);
126
+ });
127
+ server.tool("conduyt_delete_ring_group", "Delete a ring group. Its number keeps ringing default routing afterwards.", {
128
+ id: z.string().describe("Ring group UUID"),
129
+ }, async ({ id }) => {
130
+ const result = await client.del(`/api/v1/ring-groups/${id}`);
131
+ return formatResult(result);
132
+ });
133
+ server.tool("conduyt_release_ring_group_number", "Release a phone number from ring-group routing (DESTRUCTIVE). confirm must be the EXACT phone number repeated as a string. A number still referenced by an ACTIVE ring group cannot be released — delete or repoint that group first; force does NOT bypass it. force exists only for numbers whose Twilio ownership cannot be verified, and then a non-empty forceReason is required.", {
134
+ phoneNumber: z.string().describe("E.164 number to release"),
135
+ confirm: z.string().describe("EXACT repetition of phoneNumber (string) — the API rejects anything else"),
136
+ force: z.boolean().optional().describe("ONLY when Twilio ownership is unverifiable; requires forceReason"),
137
+ forceReason: z.string().optional().describe("Non-empty justification, required with force"),
138
+ }, async (params) => {
139
+ const result = await client.post("/api/v1/ring-groups/release-number", params);
140
+ return formatResult(result);
141
+ });
142
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt-mcp",
3
- "version": "4.20.0",
3
+ "version": "4.21.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",
@@ -506,14 +506,9 @@
506
506
  "reason": "automation-folder organization (shipped 2026-08) \u2014 agent-relevant workflow housekeeping; typed tools wanted"
507
507
  },
508
508
  {
509
- "match": "/api/v1/call-flows",
510
- "status": "planned",
511
- "reason": "call-flow builder API (shipped 2026-08-21) \u2014 agent-relevant; typed tools follow once the canvas UI (#25) settles the surface"
512
- },
513
- {
514
- "match": "/api/v1/ring-groups",
515
- "status": "planned",
516
- "reason": "ring-group management (call-flows companion, shipped 2026-08-21) \u2014 agent-relevant; typed tools with the call-flow set"
509
+ "match": "POST /api/v1/call-flows/audio",
510
+ "status": "excluded",
511
+ "reason": "multipart file upload (IVR audio) - not expressible as a text MCP tool; agents reference existing audio by id from conduyt_get_call_flow"
517
512
  }
518
513
  ]
519
514
  }
@@ -1,11 +1,11 @@
1
1
  {
2
- "generatedAt": "2026-08-22T18:59:24.922Z",
3
- "toolCount": 173,
2
+ "generatedAt": "2026-08-23T13:00:42.998Z",
3
+ "toolCount": 189,
4
4
  "totalEndpoints": 872,
5
5
  "counts": {
6
- "covered": 161,
7
- "excluded": 255,
8
- "planned": 456,
6
+ "covered": 177,
7
+ "excluded": 256,
8
+ "planned": 439,
9
9
  "uncategorized": 0
10
10
  },
11
11
  "matrix": {
@@ -991,48 +991,68 @@
991
991
  "reason": "calendar/booking management beyond current appointment tools"
992
992
  },
993
993
  "GET /api/v1/call-flows": {
994
- "status": "planned",
995
- "reason": "call-flow builder API (shipped 2026-08-21) — agent-relevant; typed tools follow once the canvas UI (#25) settles the surface"
994
+ "status": "covered",
995
+ "tools": [
996
+ "conduyt_list_call_flows"
997
+ ]
996
998
  },
997
999
  "POST /api/v1/call-flows": {
998
- "status": "planned",
999
- "reason": "call-flow builder API (shipped 2026-08-21) — agent-relevant; typed tools follow once the canvas UI (#25) settles the surface"
1000
+ "status": "covered",
1001
+ "tools": [
1002
+ "conduyt_create_call_flow"
1003
+ ]
1000
1004
  },
1001
1005
  "GET /api/v1/call-flows/:id": {
1002
- "status": "planned",
1003
- "reason": "call-flow builder API (shipped 2026-08-21) — agent-relevant; typed tools follow once the canvas UI (#25) settles the surface"
1006
+ "status": "covered",
1007
+ "tools": [
1008
+ "conduyt_get_call_flow"
1009
+ ]
1004
1010
  },
1005
1011
  "PATCH /api/v1/call-flows/:id": {
1006
- "status": "planned",
1007
- "reason": "call-flow builder API (shipped 2026-08-21) — agent-relevant; typed tools follow once the canvas UI (#25) settles the surface"
1012
+ "status": "covered",
1013
+ "tools": [
1014
+ "conduyt_update_call_flow"
1015
+ ]
1008
1016
  },
1009
1017
  "DELETE /api/v1/call-flows/:id": {
1010
- "status": "planned",
1011
- "reason": "call-flow builder API (shipped 2026-08-21) — agent-relevant; typed tools follow once the canvas UI (#25) settles the surface"
1018
+ "status": "covered",
1019
+ "tools": [
1020
+ "conduyt_delete_call_flow"
1021
+ ]
1012
1022
  },
1013
1023
  "POST /api/v1/call-flows/:id/publish": {
1014
- "status": "planned",
1015
- "reason": "call-flow builder API (shipped 2026-08-21) — agent-relevant; typed tools follow once the canvas UI (#25) settles the surface"
1024
+ "status": "covered",
1025
+ "tools": [
1026
+ "conduyt_publish_call_flow"
1027
+ ]
1016
1028
  },
1017
1029
  "POST /api/v1/call-flows/:id/simulate": {
1018
- "status": "planned",
1019
- "reason": "call-flow builder API (shipped 2026-08-21) — agent-relevant; typed tools follow once the canvas UI (#25) settles the surface"
1030
+ "status": "covered",
1031
+ "tools": [
1032
+ "conduyt_simulate_call_flow"
1033
+ ]
1020
1034
  },
1021
1035
  "POST /api/v1/call-flows/:id/unpublish": {
1022
- "status": "planned",
1023
- "reason": "call-flow builder API (shipped 2026-08-21) — agent-relevant; typed tools follow once the canvas UI (#25) settles the surface"
1036
+ "status": "covered",
1037
+ "tools": [
1038
+ "conduyt_unpublish_call_flow"
1039
+ ]
1024
1040
  },
1025
1041
  "GET /api/v1/call-flows/:id/validate": {
1026
- "status": "planned",
1027
- "reason": "call-flow builder API (shipped 2026-08-21) — agent-relevant; typed tools follow once the canvas UI (#25) settles the surface"
1042
+ "status": "covered",
1043
+ "tools": [
1044
+ "conduyt_validate_call_flow"
1045
+ ]
1028
1046
  },
1029
1047
  "POST /api/v1/call-flows/audio": {
1030
- "status": "planned",
1031
- "reason": "call-flow builder API (shipped 2026-08-21) — agent-relevant; typed tools follow once the canvas UI (#25) settles the surface"
1048
+ "status": "excluded",
1049
+ "reason": "multipart file upload (IVR audio) - not expressible as a text MCP tool; agents reference existing audio by id from conduyt_get_call_flow"
1032
1050
  },
1033
1051
  "GET /api/v1/call-flows/roster": {
1034
- "status": "planned",
1035
- "reason": "call-flow builder API (shipped 2026-08-21) — agent-relevant; typed tools follow once the canvas UI (#25) settles the surface"
1052
+ "status": "covered",
1053
+ "tools": [
1054
+ "conduyt_call_flow_roster"
1055
+ ]
1036
1056
  },
1037
1057
  "GET /api/v1/calls": {
1038
1058
  "status": "covered",
@@ -3030,28 +3050,40 @@
3030
3050
  "reason": "additional report surfaces beyond ai/insights"
3031
3051
  },
3032
3052
  "GET /api/v1/ring-groups": {
3033
- "status": "planned",
3034
- "reason": "ring-group management (call-flows companion, shipped 2026-08-21) — agent-relevant; typed tools with the call-flow set"
3053
+ "status": "covered",
3054
+ "tools": [
3055
+ "conduyt_list_ring_groups"
3056
+ ]
3035
3057
  },
3036
3058
  "POST /api/v1/ring-groups": {
3037
- "status": "planned",
3038
- "reason": "ring-group management (call-flows companion, shipped 2026-08-21) — agent-relevant; typed tools with the call-flow set"
3059
+ "status": "covered",
3060
+ "tools": [
3061
+ "conduyt_create_ring_group"
3062
+ ]
3039
3063
  },
3040
3064
  "GET /api/v1/ring-groups/:id": {
3041
- "status": "planned",
3042
- "reason": "ring-group management (call-flows companion, shipped 2026-08-21) — agent-relevant; typed tools with the call-flow set"
3065
+ "status": "covered",
3066
+ "tools": [
3067
+ "conduyt_get_ring_group"
3068
+ ]
3043
3069
  },
3044
3070
  "PATCH /api/v1/ring-groups/:id": {
3045
- "status": "planned",
3046
- "reason": "ring-group management (call-flows companion, shipped 2026-08-21) — agent-relevant; typed tools with the call-flow set"
3071
+ "status": "covered",
3072
+ "tools": [
3073
+ "conduyt_update_ring_group"
3074
+ ]
3047
3075
  },
3048
3076
  "DELETE /api/v1/ring-groups/:id": {
3049
- "status": "planned",
3050
- "reason": "ring-group management (call-flows companion, shipped 2026-08-21) — agent-relevant; typed tools with the call-flow set"
3077
+ "status": "covered",
3078
+ "tools": [
3079
+ "conduyt_delete_ring_group"
3080
+ ]
3051
3081
  },
3052
3082
  "POST /api/v1/ring-groups/release-number": {
3053
- "status": "planned",
3054
- "reason": "ring-group management (call-flows companion, shipped 2026-08-21) — agent-relevant; typed tools with the call-flow set"
3083
+ "status": "covered",
3084
+ "tools": [
3085
+ "conduyt_release_ring_group_number"
3086
+ ]
3055
3087
  },
3056
3088
  "GET /api/v1/saved-filters": {
3057
3089
  "status": "planned",