conduyt-mcp 4.82.0 → 4.83.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
@@ -112,6 +112,8 @@ MCP server for [Conduyt CRM](https://conduyt.app) — expose your CRM as AI-acce
112
112
  | `conduyt_send_email` | Send a single email to a contact |
113
113
  | `conduyt_check_message` | Grade a draft text or email before sending (`POST /messages/check`): score, grade, verdict, flagged words with safer alternatives (Unicode code-point offsets), checks, a text's segment count. Educates, never blocks; nothing stored or sent. `context` template / first / conversation (pick from `hasOutboundSms`); `provider: "twilio"` (text only; the notices in Twilio's wording; an email with a provider is a 400) and `clean: true` (adds the cleaner's proposal); the answer carries `breakdown`, `notices`, `unicodeCulprits` and `engine.version` (2.1.2); 20 per minute per account; 400 / 403 / 413 / 429 surfaced verbatim, never retried |
114
114
  | `conduyt_list_sms_provider_options` | The workspace's active SMS providers, redacted (`GET /sms-providers/template-options`): `{ data: [{ id, name, color }] }`, nothing else. Needs `email_templates.view` or `settings.view` (scopes emails / sms / messages / settings); read-only |
115
+ | `conduyt_get_message_check_minimum_grade` | The admin's Message check minimum grade to send (`settings.messageCheck.minimumGrade`): `enabled`, per-channel `sms` / `email` minimum (letter A-F, score 0-100 or null) and `adminOverride`. Groundwork: stored and read back, not enforced yet. Owner / admin only (anyone else gets `visible: false`); settings scope |
116
+ | `conduyt_update_message_check_minimum_grade` | Change it (`PATCH /settings`): `enabled`, `smsMinimum`, `emailMinimum`, `adminOverride`; sends the whole block with the settings revision it read. `enabled: true` is refused 400 `minimum_grade_not_available_yet`; a non-owner / non-admin gets 403 `forbidden`; 422 for E, fractions, out-of-range scores; all verbatim, never retried |
115
117
  | `conduyt_log_message` | Record a message on a contact's timeline; an inbound SMS STOP or START records the number's opt-out or opt-in and answers smsConsent |
116
118
 
117
119
  ### Calendly
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { registerAiTools } from "./tools/ai.js";
13
13
  import { registerAutomationTools } from "./tools/automations.js";
14
14
  import { registerSequenceTools } from "./tools/sequences.js";
15
15
  import { registerMessagingTools } from "./tools/messaging.js";
16
+ import { registerMessageCheckSettingsTools } from "./tools/message-check-settings.js";
16
17
  import { registerCallTools } from "./tools/calls.js";
17
18
  import { registerDialerSettingsTools } from "./tools/dialer-settings.js";
18
19
  import { registerCompanyTools } from "./tools/companies.js";
@@ -91,6 +92,7 @@ registerAiTools(server, client);
91
92
  registerAutomationTools(server, client);
92
93
  registerSequenceTools(server, client);
93
94
  registerMessagingTools(server, client);
95
+ registerMessageCheckSettingsTools(server, client);
94
96
  registerCallTools(server, client);
95
97
  registerDialerSettingsTools(server, client);
96
98
  registerCompanyTools(server, client);
@@ -1,3 +1,5 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { ConduytClient } from "../client.js";
3
+ /** (#136, Codex r1 m2) POST /bulk/contacts/update is NOT atomic and answers per row: an impossible phone is a per-row success:false inside HTTP 200 (no code) while other rows commit; the route takes no country */
4
+ export declare const BULK_UPDATE_REFUSALS = " Results are PER ROW inside an HTTP 200 (not atomic: some rows can be saved while others fail): read data.summary { total, succeeded, failed } and data.results, each { id, success, error? }. An impossible US/Canada number (area code or exchange starting 0 or 1) fails only that row, success: false with the error text (no phone_invalid code, no top-level 400), and nothing is saved for that row. This route does not take country: a country-only row fails as a row, and a country sent beside another field is ignored while that field is applied (use conduyt_bulk_edit_contacts for a country; it refuses a null or blank country 400 country_invalid).";
3
5
  export declare function registerBulkTools(server: McpServer, client: ConduytClient): void;
@@ -1,5 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { formatResult } from "../client.js";
3
+ import { INVALID_NUMBER_REFUSALS } from "./contacts.js";
4
+ /** (#136, Codex r1 m2) POST /bulk/contacts/update is NOT atomic and answers per row: an impossible phone is a per-row success:false inside HTTP 200 (no code) while other rows commit; the route takes no country */
5
+ export const BULK_UPDATE_REFUSALS = " Results are PER ROW inside an HTTP 200 (not atomic: some rows can be saved while others fail): read data.summary { total, succeeded, failed } and data.results, each { id, success, error? }. An impossible US/Canada number (area code or exchange starting 0 or 1) fails only that row, success: false with the error text (no phone_invalid code, no top-level 400), and nothing is saved for that row. This route does not take country: a country-only row fails as a row, and a country sent beside another field is ignored while that field is applied (use conduyt_bulk_edit_contacts for a country; it refuses a null or blank country 400 country_invalid).";
3
6
  export function registerBulkTools(server, client) {
4
7
  server.tool("conduyt_ai_import_contacts", "AI-powered bulk contact import. Accepts up to 1000 contacts with optional auto-tagging, smart list creation, and automation enrollment. Deduplicates by email, phone, or both.", {
5
8
  contacts: z.array(z.object({
@@ -94,14 +97,14 @@ export function registerBulkTools(server, client) {
94
97
  const result = await client.post("/api/v1/bulk/contacts/untag", params);
95
98
  return formatResult(result);
96
99
  });
97
- server.tool("conduyt_bulk_edit_contacts", "Apply the SAME field changes to many contacts at once (one updates object → all ids). For different values per contact, use conduyt_bulk_update_contacts. Requires contacts:edit.", {
100
+ server.tool("conduyt_bulk_edit_contacts", "Apply the SAME field changes to many contacts at once (one updates object → all ids). For different values per contact, use conduyt_bulk_update_contacts. Requires contacts:edit." + INVALID_NUMBER_REFUSALS, {
98
101
  ids: z.array(z.string()).describe("Contact UUIDs to edit"),
99
102
  updates: z.record(z.any()).describe("Fields to set on every listed contact, e.g. { assignedTo: '<uuid>', source: 'webinar', customFields: { tier: 'gold' } }. customFields merge into existing values."),
100
103
  }, async (params) => {
101
104
  const result = await client.post("/api/v1/bulk/contacts/edit", params);
102
105
  return formatResult(result);
103
106
  });
104
- server.tool("conduyt_bulk_update_contacts", "Apply DIFFERENT field changes per contact in one call (each row carries its own id + fields). For the same change across many, use conduyt_bulk_edit_contacts. Requires contacts:edit.", {
107
+ server.tool("conduyt_bulk_update_contacts", "Apply DIFFERENT field changes per contact in one call (each row carries its own id + fields). For the same change across many, use conduyt_bulk_edit_contacts. Requires contacts:edit." + BULK_UPDATE_REFUSALS, {
105
108
  updates: z
106
109
  .array(z.record(z.any()))
107
110
  .describe("Array of per-contact update objects; each MUST include 'id' plus the fields to change, e.g. [{ id: '<uuid>', firstName: 'Ada' }, …]"),
@@ -2,4 +2,6 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { ConduytClient } from "../client.js";
3
3
  /** The contact save revision pin (catalog bounds: integer 0..2147483647). */
4
4
  export declare const CONTACT_REVISION_MAX = 2147483647;
5
+ /** (#136, app 10-10) the refusals the contact writes answer for a number or a country the API cannot accept: surfaced verbatim, never retried */
6
+ export declare const INVALID_NUMBER_REFUSALS = " An impossible US/Canada number (area code or exchange starting 0 or 1) is refused 400 phone_invalid and nothing is saved; a country is never null or blank (400 country_invalid on an update, 2-letter code, min 1 character). Both are surfaced verbatim and never retried.";
5
7
  export declare function registerContactTools(server: McpServer, client: ConduytClient): void;
@@ -20,6 +20,8 @@ function resolveCustomFields(camel, snake) {
20
20
  }
21
21
  return camel !== undefined ? camel : snake;
22
22
  }
23
+ /** (#136, app 10-10) the refusals the contact writes answer for a number or a country the API cannot accept: surfaced verbatim, never retried */
24
+ export const INVALID_NUMBER_REFUSALS = " An impossible US/Canada number (area code or exchange starting 0 or 1) is refused 400 phone_invalid and nothing is saved; a country is never null or blank (400 country_invalid on an update, 2-letter code, min 1 character). Both are surfaced verbatim and never retried.";
23
25
  export function registerContactTools(server, client) {
24
26
  server.tool("conduyt_list_contacts", "List and search contacts with filtering. Returns paginated results with name, email, phone, tags, source, and assignee. Each row carries `revision` (integer save revision; pass it back as expectedRevision on conduyt_update_contact or the collaborator tools).", {
25
27
  search: z.string().optional().describe("Search by name, email, or phone number"),
@@ -57,7 +59,7 @@ export function registerContactTools(server, client) {
57
59
  const result = await client.get(`/api/v1/contacts/${id}`);
58
60
  return formatResult(result);
59
61
  });
60
- server.tool("conduyt_create_contact", "Create a new contact in the CRM. Email or phone is recommended but not strictly required.", {
62
+ server.tool("conduyt_create_contact", "Create a new contact in the CRM. Email or phone is recommended but not strictly required." + INVALID_NUMBER_REFUSALS, {
61
63
  email: z.string().optional().describe("Contact email address"),
62
64
  firstName: z.string().optional().describe("First name"),
63
65
  lastName: z.string().optional().describe("Last name"),
@@ -75,7 +77,7 @@ export function registerContactTools(server, client) {
75
77
  const result = await client.post("/api/v1/contacts", body);
76
78
  return formatResult(result);
77
79
  });
78
- strictInput(server.tool("conduyt_update_contact", "Update an existing contact's fields — name, email, phone, company, custom fields. customFields merge into existing values (omit a key to leave it unchanged). Pass expectedRevision (the contact's `revision` as you read it) to refuse the save when the contact changed since. " + CONTACT_CHANGED, {
80
+ strictInput(server.tool("conduyt_update_contact", "Update an existing contact's fields — name, email, phone, company, custom fields. customFields merge into existing values (omit a key to leave it unchanged). Pass expectedRevision (the contact's `revision` as you read it) to refuse the save when the contact changed since." + INVALID_NUMBER_REFUSALS + " " + CONTACT_CHANGED, {
79
81
  id: z.string().describe("Contact UUID"),
80
82
  email: z.string().optional().describe("Updated email"),
81
83
  firstName: z.string().optional().describe("Updated first name"),
@@ -0,0 +1,23 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { ConduytClient } from "../client.js";
4
+ /**
5
+ * Message check minimum grade, #134 groundwork (app 10-10, LIVE 9:19 AM): the admin's lowest acceptable Message check result per
6
+ * channel, stored in the account settings as settings.messageCheck.minimumGrade. NOTHING enforces it yet (Send stays enabled for
7
+ * everyone); enabled: true is refused 400 minimum_grade_not_available_yet until the feature is switched on. Owner / admin only:
8
+ * GET /settings leaves the block out for anyone else and PATCH answers 403 forbidden. No new scope: the existing `settings` scope.
9
+ */
10
+ export declare const MINIMUM_GRADE_NOTE = "Groundwork only: the minimum is stored and read back but NOT enforced yet (Send stays enabled for everyone), and enabled: true is refused 400 minimum_grade_not_available_yet until the feature is switched on.";
11
+ export declare const MinimumGradeChannelValue: z.ZodUnion<[z.ZodEnum<["A", "B", "C", "D", "F"]>, z.ZodNumber, z.ZodNull]>;
12
+ type Block = {
13
+ enabled: boolean;
14
+ channel: {
15
+ sms: unknown;
16
+ email: unknown;
17
+ };
18
+ adminOverride: boolean;
19
+ };
20
+ /** the stored block with the defaults laid under it field by field (never throws); null when the key's member is not an owner / admin (the API leaves the block out) */
21
+ export declare function minimumGradeOf(settings: Record<string, unknown> | undefined): Block | null;
22
+ export declare function registerMessageCheckSettingsTools(server: McpServer, client: ConduytClient): void;
23
+ export {};
@@ -0,0 +1,62 @@
1
+ import { z } from "zod";
2
+ import { formatResult } from "../client.js";
3
+ import { patchGeneralSettingsMerged } from "./lifecycle.js";
4
+ import { strictInput } from "./strict.js";
5
+ /**
6
+ * Message check minimum grade, #134 groundwork (app 10-10, LIVE 9:19 AM): the admin's lowest acceptable Message check result per
7
+ * channel, stored in the account settings as settings.messageCheck.minimumGrade. NOTHING enforces it yet (Send stays enabled for
8
+ * everyone); enabled: true is refused 400 minimum_grade_not_available_yet until the feature is switched on. Owner / admin only:
9
+ * GET /settings leaves the block out for anyone else and PATCH answers 403 forbidden. No new scope: the existing `settings` scope.
10
+ */
11
+ export const MINIMUM_GRADE_NOTE = "Groundwork only: the minimum is stored and read back but NOT enforced yet (Send stays enabled for everyone), and enabled: true is refused 400 minimum_grade_not_available_yet until the feature is switched on.";
12
+ export const MinimumGradeChannelValue = z
13
+ .union([z.enum(["A", "B", "C", "D", "F"]), z.number().int().min(0).max(100), z.null()])
14
+ .describe("Lowest acceptable result: a grade letter (A, B, C, D, F), a whole score 0-100, or null for no minimum (E, fractions and out-of-range scores are refused 422)");
15
+ const DEFAULT_BLOCK = { enabled: false, channel: { sms: null, email: null }, adminOverride: true };
16
+ /** the stored block with the defaults laid under it field by field (never throws); null when the key's member is not an owner / admin (the API leaves the block out) */
17
+ export function minimumGradeOf(settings) {
18
+ const mc = settings?.messageCheck;
19
+ if (!mc || typeof mc !== "object" || Array.isArray(mc))
20
+ return null;
21
+ const mg = mc.minimumGrade;
22
+ const s = mg && typeof mg === "object" && !Array.isArray(mg) ? mg : {};
23
+ const ch = s.channel && typeof s.channel === "object" && !Array.isArray(s.channel) ? s.channel : {};
24
+ return {
25
+ enabled: s.enabled === true,
26
+ channel: { sms: ch.sms ?? null, email: ch.email ?? null },
27
+ adminOverride: s.adminOverride === false ? false : true,
28
+ };
29
+ }
30
+ export function registerMessageCheckSettingsTools(server, client) {
31
+ server.tool("conduyt_get_message_check_minimum_grade", `Read the admin's Message check minimum grade to send (GET /api/v1/settings, settings.messageCheck.minimumGrade): { data: { minimumGrade: { enabled, channel: { sms, email }, adminOverride } | null, visible, note } }. enabled is the master switch (default false); channel.sms / channel.email is the lowest acceptable result for that channel (a letter A-F, a score 0-100 or null = no minimum); adminOverride says whether an owner or admin may send below it (default true). ${MINIMUM_GRADE_NOTE} Owner / admin only: for any other member the API leaves the block out, so minimumGrade is null and visible false (that is a redaction, not an unset minimum). Requires the settings scope.`, {}, async () => {
32
+ const result = (await client.get("/api/v1/settings"));
33
+ const block = minimumGradeOf(result?.data);
34
+ return formatResult({ data: { minimumGrade: block, visible: block !== null, note: MINIMUM_GRADE_NOTE } });
35
+ });
36
+ strictInput(server.tool("conduyt_update_message_check_minimum_grade", `Change the Message check minimum grade to send (PATCH /api/v1/settings, settings.messageCheck.minimumGrade). Pass only what changes; the tool reads the stored block and sends the WHOLE block (the API replaces it) with the settings revision it read; a 409 (another save landed first) re-reads once, then hands back the current settings. smsMinimum / emailMinimum: a letter A-F, a whole score 0-100, or null for no minimum; adminOverride: whether an owner or admin may send below the minimum; enabled: the master switch. ${MINIMUM_GRADE_NOTE} Owner / admin only: a 403 forbidden ('Only an owner or admin can change the Message check minimum grade') is surfaced verbatim, as are 400 minimum_grade_not_available_yet and 422 invalid_body (a partial block, an unknown key, E, a fraction, a score outside 0-100). Requires settings:edit on the key's member AND the owner or admin role. No new scope: the existing settings scope.`, {
37
+ enabled: z.boolean().optional().describe("Master switch. true is refused 400 minimum_grade_not_available_yet until the feature is switched on"),
38
+ smsMinimum: MinimumGradeChannelValue.optional().describe("SMS minimum: A-F, 0-100, or null for none"),
39
+ emailMinimum: MinimumGradeChannelValue.optional().describe("Email minimum: A-F, 0-100, or null for none"),
40
+ adminOverride: z.boolean().optional().describe("Whether an owner or admin may send below the minimum (default true)"),
41
+ }, async (params) => {
42
+ if (params.enabled === undefined && params.smsMinimum === undefined && params.emailMinimum === undefined && params.adminOverride === undefined) {
43
+ throw new Error("nothing to change: pass at least one of enabled, smsMinimum, emailMinimum, adminOverride");
44
+ }
45
+ return patchGeneralSettingsMerged(client, (settings) => {
46
+ // a member who cannot see the block gets the defaults laid under the change; the API then answers 403 forbidden verbatim
47
+ const stored = minimumGradeOf(settings) ?? DEFAULT_BLOCK;
48
+ return {
49
+ messageCheck: {
50
+ minimumGrade: {
51
+ enabled: params.enabled ?? stored.enabled,
52
+ channel: {
53
+ sms: params.smsMinimum === undefined ? stored.channel.sms : params.smsMinimum,
54
+ email: params.emailMinimum === undefined ? stored.channel.email : params.emailMinimum,
55
+ },
56
+ adminOverride: params.adminOverride ?? stored.adminOverride,
57
+ },
58
+ },
59
+ };
60
+ });
61
+ }));
62
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt-mcp",
3
- "version": "4.82.0",
3
+ "version": "4.83.0",
4
4
  "mcpName": "com.conduyt/mcp",
5
5
  "description": "MCP server for Conduyt CRM — expose CRM operations as AI-accessible tools",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
- "generatedAt": "2026-10-10T01:13:48.576Z",
3
- "toolCount": 347,
2
+ "generatedAt": "2026-10-10T17:00:26.027Z",
3
+ "toolCount": 349,
4
4
  "totalEndpoints": 1077,
5
5
  "counts": {
6
6
  "covered": 308,
@@ -4031,7 +4031,8 @@
4031
4031
  "conduyt_get_lead_pool_settings",
4032
4032
  "conduyt_update_lead_pool_settings",
4033
4033
  "conduyt_get_lifecycle_settings",
4034
- "conduyt_update_agent_statuses"
4034
+ "conduyt_update_agent_statuses",
4035
+ "conduyt_get_message_check_minimum_grade"
4035
4036
  ]
4036
4037
  },
4037
4038
  "PATCH /api/v1/settings": {