conduyt-mcp 4.19.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
+ }
@@ -41,6 +41,12 @@ export function registerUserTools(server, client) {
41
41
  phone: z.string().optional().describe("Updated phone"),
42
42
  role: z.enum(["member", "admin", "owner"]).optional().describe("Updated role"),
43
43
  permissions: z.array(z.string()).optional().describe("Updated permission scopes"),
44
+ position: z.string().nullable().optional().describe("Rep-profile position/title, merge-taggable as {{user.position}} (owner/admin only; null clears)"),
45
+ mobilePhone: z.string().nullable().optional().describe("Rep-profile mobile phone, merge-taggable as {{user.mobilePhone}} (owner/admin only; null clears)"),
46
+ custom1: z.string().nullable().optional().describe("Rep-profile custom field 1, merge-taggable as {{user.custom1}} (owner/admin only; null clears)"),
47
+ custom2: z.string().nullable().optional().describe("Rep-profile custom field 2, merge-taggable as {{user.custom2}} (owner/admin only; null clears)"),
48
+ custom3: z.string().nullable().optional().describe("Rep-profile custom field 3, merge-taggable as {{user.custom3}} (owner/admin only; null clears)"),
49
+ custom4: z.string().nullable().optional().describe("Rep-profile custom field 4, merge-taggable as {{user.custom4}} (owner/admin only; null clears)"),
44
50
  }, async ({ id, ...updates }) => {
45
51
  const result = await client.patch(`/api/v1/users/${id}`, updates);
46
52
  return formatResult(result);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt-mcp",
3
- "version": "4.19.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",
@@ -499,6 +499,16 @@
499
499
  "match": "/api/v1/webhooks",
500
500
  "status": "planned",
501
501
  "reason": "outbound webhook subscription management (distinct from the inbound /webhooks/ receivers)"
502
+ },
503
+ {
504
+ "match": "/api/v1/automation-folders",
505
+ "status": "planned",
506
+ "reason": "automation-folder organization (shipped 2026-08) \u2014 agent-relevant workflow housekeeping; typed tools wanted"
507
+ },
508
+ {
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"
502
512
  }
503
513
  ]
504
- }
514
+ }
@@ -1,11 +1,11 @@
1
1
  {
2
- "generatedAt": "2026-08-06T22:46:04.536Z",
3
- "toolCount": 173,
4
- "totalEndpoints": 842,
2
+ "generatedAt": "2026-08-23T13:00:42.998Z",
3
+ "toolCount": 189,
4
+ "totalEndpoints": 872,
5
5
  "counts": {
6
- "covered": 161,
7
- "excluded": 248,
8
- "planned": 433,
6
+ "covered": 177,
7
+ "excluded": 256,
8
+ "planned": 439,
9
9
  "uncategorized": 0
10
10
  },
11
11
  "matrix": {
@@ -395,6 +395,10 @@
395
395
  "status": "planned",
396
396
  "reason": "API key admin beyond self-check"
397
397
  },
398
+ "GET /api/v1/api-keys/:id": {
399
+ "status": "planned",
400
+ "reason": "API key admin beyond self-check"
401
+ },
398
402
  "PATCH /api/v1/api-keys/:id": {
399
403
  "status": "planned",
400
404
  "reason": "API key admin beyond self-check"
@@ -533,6 +537,22 @@
533
537
  "status": "planned",
534
538
  "reason": "execution inspection"
535
539
  },
540
+ "GET /api/v1/automation-folders": {
541
+ "status": "planned",
542
+ "reason": "automation-folder organization (shipped 2026-08) — agent-relevant workflow housekeeping; typed tools wanted"
543
+ },
544
+ "POST /api/v1/automation-folders": {
545
+ "status": "planned",
546
+ "reason": "automation-folder organization (shipped 2026-08) — agent-relevant workflow housekeeping; typed tools wanted"
547
+ },
548
+ "PATCH /api/v1/automation-folders/:id": {
549
+ "status": "planned",
550
+ "reason": "automation-folder organization (shipped 2026-08) — agent-relevant workflow housekeeping; typed tools wanted"
551
+ },
552
+ "DELETE /api/v1/automation-folders/:id": {
553
+ "status": "planned",
554
+ "reason": "automation-folder organization (shipped 2026-08) — agent-relevant workflow housekeeping; typed tools wanted"
555
+ },
536
556
  "GET /api/v1/automations": {
537
557
  "status": "covered",
538
558
  "tools": [
@@ -970,6 +990,70 @@
970
990
  "status": "planned",
971
991
  "reason": "calendar/booking management beyond current appointment tools"
972
992
  },
993
+ "GET /api/v1/call-flows": {
994
+ "status": "covered",
995
+ "tools": [
996
+ "conduyt_list_call_flows"
997
+ ]
998
+ },
999
+ "POST /api/v1/call-flows": {
1000
+ "status": "covered",
1001
+ "tools": [
1002
+ "conduyt_create_call_flow"
1003
+ ]
1004
+ },
1005
+ "GET /api/v1/call-flows/:id": {
1006
+ "status": "covered",
1007
+ "tools": [
1008
+ "conduyt_get_call_flow"
1009
+ ]
1010
+ },
1011
+ "PATCH /api/v1/call-flows/:id": {
1012
+ "status": "covered",
1013
+ "tools": [
1014
+ "conduyt_update_call_flow"
1015
+ ]
1016
+ },
1017
+ "DELETE /api/v1/call-flows/:id": {
1018
+ "status": "covered",
1019
+ "tools": [
1020
+ "conduyt_delete_call_flow"
1021
+ ]
1022
+ },
1023
+ "POST /api/v1/call-flows/:id/publish": {
1024
+ "status": "covered",
1025
+ "tools": [
1026
+ "conduyt_publish_call_flow"
1027
+ ]
1028
+ },
1029
+ "POST /api/v1/call-flows/:id/simulate": {
1030
+ "status": "covered",
1031
+ "tools": [
1032
+ "conduyt_simulate_call_flow"
1033
+ ]
1034
+ },
1035
+ "POST /api/v1/call-flows/:id/unpublish": {
1036
+ "status": "covered",
1037
+ "tools": [
1038
+ "conduyt_unpublish_call_flow"
1039
+ ]
1040
+ },
1041
+ "GET /api/v1/call-flows/:id/validate": {
1042
+ "status": "covered",
1043
+ "tools": [
1044
+ "conduyt_validate_call_flow"
1045
+ ]
1046
+ },
1047
+ "POST /api/v1/call-flows/audio": {
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"
1050
+ },
1051
+ "GET /api/v1/call-flows/roster": {
1052
+ "status": "covered",
1053
+ "tools": [
1054
+ "conduyt_call_flow_roster"
1055
+ ]
1056
+ },
973
1057
  "GET /api/v1/calls": {
974
1058
  "status": "covered",
975
1059
  "tools": [
@@ -1381,6 +1465,10 @@
1381
1465
  "status": "excluded",
1382
1466
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1383
1467
  },
1468
+ "GET /api/v1/cron/process-automation-redispatch": {
1469
+ "status": "excluded",
1470
+ "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1471
+ },
1384
1472
  "GET /api/v1/cron/process-batches": {
1385
1473
  "status": "excluded",
1386
1474
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
@@ -1389,6 +1477,10 @@
1389
1477
  "status": "excluded",
1390
1478
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1391
1479
  },
1480
+ "GET /api/v1/cron/process-call-lifecycle-intents": {
1481
+ "status": "excluded",
1482
+ "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1483
+ },
1392
1484
  "GET /api/v1/cron/process-consent-outbox": {
1393
1485
  "status": "excluded",
1394
1486
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
@@ -1489,6 +1581,10 @@
1489
1581
  "status": "excluded",
1490
1582
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1491
1583
  },
1584
+ "GET /api/v1/cron/reap-stale-group-calls": {
1585
+ "status": "excluded",
1586
+ "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1587
+ },
1492
1588
  "GET /api/v1/cron/reconcile-document-sends": {
1493
1589
  "status": "excluded",
1494
1590
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
@@ -1501,6 +1597,10 @@
1501
1597
  "status": "excluded",
1502
1598
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1503
1599
  },
1600
+ "GET /api/v1/cron/recover-booking-side-effects": {
1601
+ "status": "excluded",
1602
+ "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1603
+ },
1504
1604
  "GET /api/v1/cron/recurring-tasks": {
1505
1605
  "status": "excluded",
1506
1606
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
@@ -1513,6 +1613,10 @@
1513
1613
  "status": "excluded",
1514
1614
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1515
1615
  },
1616
+ "GET /api/v1/cron/reply-capture-drain": {
1617
+ "status": "excluded",
1618
+ "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1619
+ },
1516
1620
  "GET /api/v1/cron/resume-imports": {
1517
1621
  "status": "excluded",
1518
1622
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
@@ -2377,6 +2481,10 @@
2377
2481
  "status": "planned",
2378
2482
  "reason": "integration management"
2379
2483
  },
2484
+ "POST /api/v1/integrations/dpp/ingest": {
2485
+ "status": "planned",
2486
+ "reason": "integration management"
2487
+ },
2380
2488
  "GET /api/v1/integrations/health": {
2381
2489
  "status": "planned",
2382
2490
  "reason": "integration management"
@@ -2941,6 +3049,42 @@
2941
3049
  "status": "planned",
2942
3050
  "reason": "additional report surfaces beyond ai/insights"
2943
3051
  },
3052
+ "GET /api/v1/ring-groups": {
3053
+ "status": "covered",
3054
+ "tools": [
3055
+ "conduyt_list_ring_groups"
3056
+ ]
3057
+ },
3058
+ "POST /api/v1/ring-groups": {
3059
+ "status": "covered",
3060
+ "tools": [
3061
+ "conduyt_create_ring_group"
3062
+ ]
3063
+ },
3064
+ "GET /api/v1/ring-groups/:id": {
3065
+ "status": "covered",
3066
+ "tools": [
3067
+ "conduyt_get_ring_group"
3068
+ ]
3069
+ },
3070
+ "PATCH /api/v1/ring-groups/:id": {
3071
+ "status": "covered",
3072
+ "tools": [
3073
+ "conduyt_update_ring_group"
3074
+ ]
3075
+ },
3076
+ "DELETE /api/v1/ring-groups/:id": {
3077
+ "status": "covered",
3078
+ "tools": [
3079
+ "conduyt_delete_ring_group"
3080
+ ]
3081
+ },
3082
+ "POST /api/v1/ring-groups/release-number": {
3083
+ "status": "covered",
3084
+ "tools": [
3085
+ "conduyt_release_ring_group_number"
3086
+ ]
3087
+ },
2944
3088
  "GET /api/v1/saved-filters": {
2945
3089
  "status": "planned",
2946
3090
  "reason": "saved filter management"
@@ -3643,6 +3787,10 @@
3643
3787
  "status": "excluded",
3644
3788
  "reason": "inbound provider callback receivers (Twilio/Resend/etc.), not client-callable"
3645
3789
  },
3790
+ "POST /api/v1/webhooks/voice/flow-step": {
3791
+ "status": "excluded",
3792
+ "reason": "inbound provider callback receivers (Twilio/Resend/etc.), not client-callable"
3793
+ },
3646
3794
  "POST /api/v1/webhooks/voice/inbound": {
3647
3795
  "status": "excluded",
3648
3796
  "reason": "inbound provider callback receivers (Twilio/Resend/etc.), not client-callable"
@@ -3651,6 +3799,10 @@
3651
3799
  "status": "excluded",
3652
3800
  "reason": "inbound provider callback receivers (Twilio/Resend/etc.), not client-callable"
3653
3801
  },
3802
+ "POST /api/v1/webhooks/voice/ring-group-step": {
3803
+ "status": "excluded",
3804
+ "reason": "inbound provider callback receivers (Twilio/Resend/etc.), not client-callable"
3805
+ },
3654
3806
  "POST /api/v1/webhooks/voice/status": {
3655
3807
  "status": "excluded",
3656
3808
  "reason": "inbound provider callback receivers (Twilio/Resend/etc.), not client-callable"