conduyt-mcp 4.95.0 → 4.96.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
@@ -33,7 +33,7 @@ MCP server for [Conduyt CRM](https://conduyt.app) — expose your CRM as AI-acce
33
33
  | `conduyt_pipeline_health` | Pipeline health for one pipeline or all visible active pipelines (`GET /deals/inspection`): the six checks over open deals by money at risk; `check` adds that check's deals, paged by `cursor` (or `offset`). Cursor 400s carry `health_cursor_invalid` / `_context` / `_blank` / `_needs_check`, `health_filter_conflict` |
34
34
  | `conduyt_pipeline_health_fix_deal` | Pipeline health quick fix for a deal row (`PATCH /deals/{id}` with `checkOrigin`): `stale` = `stageId` (+ `expectedStageId`), `unassigned` = `assignedTo`, `missing_close_date` = `expectedCloseDate`, `missing_contact` = `contactId`; `expectedUpdatedAt` required. Refusals verbatim, never retried: 409 `deal_changed` / `deal_check_cleared` (with the deal's current health row), uncoded 409s, 400 `health_fix_mismatch` |
35
35
  | `conduyt_pipeline_health_fix_next_action` | Pipeline health "Set next action" (`POST /tasks` with `checkOrigin` `high_value_idle` / `needs_action`, `dealId`, `expectedDealUpdatedAt`, optional `idempotencyKey` sent as the `Idempotency-Key` header). At most one non-done quick-fix task per deal; refusals verbatim, never retried: 409 `deal_changed` / `deal_has_next_action` / `deal_check_cleared`, 400 `health_fix_mismatch`, idempotency 409/503 |
36
- | `conduyt_get_stage_rules` / `conduyt_set_stage_rules` | Read / set one stage's rule (`requiredFields` incl. `custom:<fieldKey>`, `requiredDocuments` 0-20, lost-reason label and options, `lostReasonAllowOther`); other stages untouched; revision-aware, retried once on a 409 |
36
+ | `conduyt_get_stage_rules` / `conduyt_set_stage_rules` | Read / set one stage's rule (`requiredFields` incl. `custom:<fieldKey>`, `requiredDocuments` 0-20, lost-reason label and options, `lostReasonAllowOther`, `entryByAutomationOnly`: only an automation can move or create a deal into the stage, every other entry is refused 422 `stage_automation_only`; `conduyt_list_pipelines` shows it on each stage); other stages untouched; revision-aware, retried once on a 409 |
37
37
  | `conduyt_get_deal_card_fields` / `conduyt_set_deal_card_fields` | Read / replace the kanban card fields (1-8, built-ins or `custom:<fieldKey>`) |
38
38
  | `conduyt_get_settings_revision` | The account `settingsRevision` (plus stored stage rules and rot days): pass it as `expectedSettingsRevision` to `conduyt_archive_pipeline` / `conduyt_delete_stage` so a stale one is refused 409 `stale_settings_revision` |
39
39
  | `conduyt_get_pipeline_rot_days` / `conduyt_set_pipeline_rot_days` | Read / set the stale-in-stage threshold per pipeline (`pipelineRotDays`, 1-90 days); only the pipelines sent change; revision-aware, retried once on a 409 |
@@ -14,7 +14,7 @@ export const MAX_DEAL_CARD_FIELDS = 8;
14
14
  export const MAX_STAGE_REQUIRED_FIELDS = 25;
15
15
  export const MAX_STAGE_REQUIRED_DOCUMENTS = 20;
16
16
  const CUSTOM_FIELD_RE = /^custom:[a-zA-Z_][a-zA-Z0-9_]*$/;
17
- const RULE_KEYS = ["requiredFields", "requiredDocuments", "lostReasonLabel", "lostReasonOptions", "lostReasonAllowOther"];
17
+ const RULE_KEYS = ["requiredFields", "requiredDocuments", "lostReasonLabel", "lostReasonOptions", "lostReasonAllowOther", "entryByAutomationOnly"];
18
18
  export const MIN_PIPELINE_ROT_DAYS = 1;
19
19
  export const MAX_PIPELINE_ROT_DAYS = 90;
20
20
  const requiredField = z.union([
@@ -27,7 +27,7 @@ const cardField = z.union([
27
27
  ]);
28
28
  const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
29
29
  export function registerBoardSettingsTools(server, client) {
30
- strictInput(server.tool("conduyt_get_stage_rules", "Read the per-stage rules (settings.pipelineStageRules) as { pipelineId: { stageId: { requiredFields, requiredDocuments, lostReasonLabel, lostReasonOptions, lostReasonAllowOther } } }, optionally for one pipeline or one stage. A deal may enter a stage only when its requiredFields are filled (built-ins value, expectedCloseDate, contactId, assignedTo, lostReason, or custom:<fieldKey> for a deal custom field) and it has at least requiredDocuments documents attached. Set one stage with conduyt_set_stage_rules. Pipelines your key cannot see are left out.", {
30
+ strictInput(server.tool("conduyt_get_stage_rules", "Read the per-stage rules (settings.pipelineStageRules) as { pipelineId: { stageId: { requiredFields, requiredDocuments, lostReasonLabel, lostReasonOptions, lostReasonAllowOther, entryByAutomationOnly } } }, optionally for one pipeline or one stage. A deal may enter a stage only when its requiredFields are filled (built-ins value, expectedCloseDate, contactId, assignedTo, lostReason, or custom:<fieldKey> for a deal custom field) and it has at least requiredDocuments documents attached. entryByAutomationOnly: true (absent = off) means only an automation run can move or create a deal INTO the stage; every other entry, admins and API keys included, is refused 422 stage_automation_only (moving out stays free). Set one stage with conduyt_set_stage_rules. Pipelines your key cannot see are left out.", {
31
31
  pipelineId: z.string().uuid().optional().describe("Only this pipeline's stage rules"),
32
32
  stageId: z.string().uuid().optional().describe("Only this stage's rule (needs pipelineId)"),
33
33
  }, async ({ pipelineId, stageId }) => {
@@ -42,7 +42,7 @@ export function registerBoardSettingsTools(server, client) {
42
42
  }
43
43
  return formatResult({ data: { pipelineStageRules: out } });
44
44
  }));
45
- strictInput(server.tool("conduyt_set_stage_rules", "Set the rule of ONE stage (settings.pipelineStageRules[pipelineId][stageId]; admin key, settings:edit). Only the fields you pass change: they are merged into that stage's current rule, and every other stage and pipeline is left exactly as stored. requiredFields REPLACES the stage's list (max 25 entries; built-ins value, expectedCloseDate, contactId, assignedTo, lostReason, or custom:<fieldKey> for one of the account's DEAL custom fields; [] clears it); requiredDocuments is the number of documents that must be attached before a deal may enter the stage (integer 0-20); lostReasonLabel (1-60 chars) and lostReasonOptions (max 50, trimmed and de-duplicated by the API) configure the stage's lost-reason prompt; lostReasonAllowOther (boolean) lets the rep also pick \"Other\" and type the reason (stored as the reason text; only takes effect while the stage has lost-reason options). Read-merge-write with the settings revision; if another save lands first it is re-read and retried once, then the conflict is returned with the current settings. An unknown custom field key, pipeline or stage is refused by the API.", {
45
+ strictInput(server.tool("conduyt_set_stage_rules", "Set the rule of ONE stage (settings.pipelineStageRules[pipelineId][stageId]; admin key, settings:edit). Only the fields you pass change: they are merged into that stage's current rule, and every other stage and pipeline is left exactly as stored. requiredFields REPLACES the stage's list (max 25 entries; built-ins value, expectedCloseDate, contactId, assignedTo, lostReason, or custom:<fieldKey> for one of the account's DEAL custom fields; [] clears it); requiredDocuments is the number of documents that must be attached before a deal may enter the stage (integer 0-20); lostReasonLabel (1-60 chars) and lostReasonOptions (max 50, trimmed and de-duplicated by the API) configure the stage's lost-reason prompt; lostReasonAllowOther (boolean) lets the rep also pick \"Other\" and type the reason (stored as the reason text; only takes effect while the stage has lost-reason options); entryByAutomationOnly (boolean) is \"Entry by automation only\": true means only an automation run can move or create a deal INTO the stage, and every route entry (deal update, create, bulk edit/update, inbound deals webhook, appointment outcome, public contact route, admins and API keys included) is refused 422 stage_automation_only with the message \"Only an automation can move a deal into <stage>\" (moving out stays free; for a manual correction set it false, move the deal, set it true again; it cannot be the starting stage of automatic intake deals). Read-merge-write with the settings revision; if another save lands first it is re-read and retried once, then the conflict is returned with the current settings. An unknown custom field key, pipeline or stage is refused by the API.", {
46
46
  pipelineId: z.string().uuid().describe("Pipeline UUID"),
47
47
  stageId: z.string().uuid().describe("Stage UUID within that pipeline"),
48
48
  requiredFields: z.array(requiredField).max(MAX_STAGE_REQUIRED_FIELDS).optional().describe("Fields a deal must have filled to enter the stage: value, expectedCloseDate, contactId, assignedTo, lostReason, or custom:<deal custom field key>. Replaces the list."),
@@ -50,10 +50,11 @@ export function registerBoardSettingsTools(server, client) {
50
50
  lostReasonLabel: z.string().trim().min(1).max(60).optional().describe("Label of the stage's lost-reason prompt, e.g. 'Disqualification Reason'"),
51
51
  lostReasonOptions: z.array(z.string()).max(50).optional().describe("Dropdown options of the lost-reason prompt (replaces the list)"),
52
52
  lostReasonAllowOther: z.boolean().optional().describe("The rep may pick 'Other' and type the reason (stored as the reason text). Needs lost-reason options to take effect."),
53
+ entryByAutomationOnly: z.boolean().optional().describe("Entry by automation only: when true, only an automation run can move or create a deal INTO this stage; every API / UI entry (admins and API keys included) is refused 422 stage_automation_only. Moving a deal out stays free. To make a manual correction, turn it off, move the deal, turn it back on."),
53
54
  }, async ({ pipelineId, stageId, ...given }) => {
54
55
  const change = Object.fromEntries(Object.entries(given).filter(([, v]) => v !== undefined));
55
56
  if (Object.keys(change).length === 0) {
56
- throw new Error("Nothing to set. Pass at least one of requiredFields, requiredDocuments, lostReasonLabel, lostReasonOptions, lostReasonAllowOther.");
57
+ throw new Error("Nothing to set. Pass at least one of requiredFields, requiredDocuments, lostReasonLabel, lostReasonOptions, lostReasonAllowOther, entryByAutomationOnly.");
57
58
  }
58
59
  return patchGeneralSettingsMerged(client, (settings) => {
59
60
  const rules = isObj(settings.pipelineStageRules) ? settings.pipelineStageRules : {};
@@ -137,14 +137,14 @@ export function registerBulkTools(server, client) {
137
137
  const result = await client.post("/api/v1/bulk/contacts/dnc", params);
138
138
  return formatResult(result);
139
139
  });
140
- server.tool("conduyt_bulk_edit_deals", "Apply the SAME field changes to many deals at once (one updates object → all ids) — e.g. reassign a set of deals to a user. For different values per deal, use conduyt_bulk_update_deals. Requires deals:edit.", {
140
+ server.tool("conduyt_bulk_edit_deals", "Apply the SAME field changes to many deals at once (one updates object → all ids) — e.g. reassign a set of deals to a user. For different values per deal, use conduyt_bulk_update_deals. Requires deals:edit. A stage change into an \"Entry by automation only\" stage is refused as code stage_automation_only with the server's message \"Only an automation can move a deal into <stage>\": the failed deal's own entry in the result carries the code and the message (per item, not a top-level error); other deals are handled on their own.", {
141
141
  ids: z.array(z.string()).describe("Deal UUIDs to edit"),
142
142
  updates: z.record(z.any()).describe("Fields to set on every listed deal, e.g. { assignedTo: '<uuid>', stageId: '<uuid>', priority: 'high' }"),
143
143
  }, async (params) => {
144
144
  const result = await client.post("/api/v1/bulk/deals/edit", params);
145
145
  return formatResult(result);
146
146
  });
147
- server.tool("conduyt_bulk_update_deals", "Apply DIFFERENT field changes per deal in one call (each row carries its own id + fields). For the same change across many, use conduyt_bulk_edit_deals. Requires deals:edit.", {
147
+ server.tool("conduyt_bulk_update_deals", "Apply DIFFERENT field changes per deal in one call (each row carries its own id + fields). For the same change across many, use conduyt_bulk_edit_deals. Requires deals:edit. A stage change into an \"Entry by automation only\" stage is refused as code stage_automation_only with the server's message \"Only an automation can move a deal into <stage>\": the failed deal's own entry in the result carries the code and the message (per item, not a top-level error); other deals are handled on their own.", {
148
148
  updates: z
149
149
  .array(z.record(z.any()))
150
150
  .describe("Array of per-deal update objects; each MUST include 'id' plus the fields to change, e.g. [{ id: '<uuid>', value: 5000 }, …]"),
@@ -199,7 +199,7 @@ export function registerDealTools(server, client) {
199
199
  const result = await client.get(`/api/v1/deals/${id}`);
200
200
  return formatResult(result);
201
201
  });
202
- server.tool("conduyt_create_deal", "Create a new deal in a pipeline with full attributes. Requires a pipeline and stage ID — use conduyt_list_pipelines to discover them. Supports currency (e.g. GBP), probability, priority, expected close date, and structured custom-field values.", {
202
+ server.tool("conduyt_create_deal", "Create a new deal in a pipeline with full attributes. Requires a pipeline and stage ID — use conduyt_list_pipelines to discover them. Supports currency (e.g. GBP), probability, priority, expected close date, and structured custom-field values. A stage set to \"Entry by automation only\" refuses every entry here: 422 code stage_automation_only, message \"Only an automation can move a deal into <stage>\" (admins and API keys included; moving out is free).", {
203
203
  title: z.string().describe("Deal title (e.g. 'Acme Corp — Enterprise Plan')"),
204
204
  pipelineId: z.string().describe("Pipeline UUID"),
205
205
  stageId: z.string().describe("Stage UUID within the pipeline"),
@@ -218,7 +218,7 @@ export function registerDealTools(server, client) {
218
218
  const result = await client.post("/api/v1/deals", params);
219
219
  return formatResult(result);
220
220
  });
221
- server.tool("conduyt_update_deal", "Update a deal — move stage, change value/currency, set probability or expected close date, reassign, mark won/lost, or write structured custom-field values. customFields are merged into existing values (omit a key to leave it unchanged).", {
221
+ server.tool("conduyt_update_deal", "Update a deal — move stage, change value/currency, set probability or expected close date, reassign, mark won/lost, or write structured custom-field values. customFields are merged into existing values (omit a key to leave it unchanged). A stage set to \"Entry by automation only\" refuses every entry here: 422 code stage_automation_only, message \"Only an automation can move a deal into <stage>\" (admins and API keys included; moving out is free).", {
222
222
  id: z.string().describe("Deal UUID"),
223
223
  title: z.string().optional().describe("Updated title"),
224
224
  stageId: z.string().optional().describe("Move to a new stage UUID"),
@@ -3,7 +3,7 @@ import { ConduytClient } from "../client.js";
3
3
  /** The 10 palette colours a pipeline can be painted (API: PIPELINE_COLOR_PALETTE; any other value is a 400). */
4
4
  export declare const PIPELINE_COLORS: readonly ["#3845C9", "#43DFF6", "#2DD6B5", "#22C58B", "#9BD13B", "#F2B544", "#F07A2F", "#E6465A", "#FD88C0", "#956EFA"];
5
5
  /** The machine-readable codes the archive route answers with (the API puts `code` in the error body; the client carries it in the error text). */
6
- export declare const ARCHIVE_ERROR_CODES = "A refusal carries a machine-readable code in the error text: stale_settings_revision (409, settings changed since expectedSettingsRevision was read; nothing moved), target_not_open (409, the destination's first stage is won/lost), dpp_post_close (409, the DPP feed's Post-Close pipeline), archive_too_large (409, too many open deals for one call: move or close some first), target_archived (409, the destination was archived meanwhile), intake_target (409, enabled intake creates its cards in this pipeline), pipeline_archived (409), open_deals (409, open deals but no moveOpenDealsTo), default_pipeline (409), stage_requirements (409, a deal fails the destination's stage-entry rules; nothing moved), archive_raced (409, retry), pipeline_automation_only (403). Archiving a pipeline that is already archived is idempotent: { archived: true, alreadyArchived: true }. The message is the server's, never swallowed. The call can take up to 75 s (a 75 s per-call timeout is used); if it still reports a timeout the archive may have committed: re-read with conduyt_list_pipelines / conduyt_list_archived_pipelines before retrying (a retry of a committed archive answers alreadyArchived, but its open deals have already moved).";
6
+ export declare const ARCHIVE_ERROR_CODES = "A refusal carries a machine-readable code in the error text: stale_settings_revision (409, settings changed since expectedSettingsRevision was read; nothing moved), target_not_open (409, the destination's first stage is won/lost), dpp_post_close (409, the DPP feed's Post-Close pipeline), archive_too_large (409, too many open deals for one call: move or close some first), target_archived (409, the destination was archived meanwhile), intake_target (409, enabled intake creates its cards in this pipeline), pipeline_archived (409), open_deals (409, open deals but no moveOpenDealsTo), default_pipeline (409), stage_requirements (409, a deal fails the destination's stage-entry rules; nothing moved), archive_raced (409, retry), pipeline_automation_only (403), stage_automation_only (422, moveOpenDealsTo lands in an \"Entry by automation only\" stage; nothing moved). Archiving a pipeline that is already archived is idempotent: { archived: true, alreadyArchived: true }. The message is the server's, never swallowed. The call can take up to 75 s (a 75 s per-call timeout is used); if it still reports a timeout the archive may have committed: re-read with conduyt_list_pipelines / conduyt_list_archived_pipelines before retrying (a retry of a committed archive answers alreadyArchived, but its open deals have already moved).";
7
7
  /** above the archive route's 60 s transaction (up to 500 deal moves) plus post-commit events; the client default is 30 s */
8
8
  export declare const ARCHIVE_PIPELINE_TIMEOUT_MS = 75000;
9
9
  export declare function registerPipelineTools(server: McpServer, client: ConduytClient): void;
@@ -5,11 +5,11 @@ import { strictInput } from "./strict.js";
5
5
  export const PIPELINE_COLORS = ["#3845C9", "#43DFF6", "#2DD6B5", "#22C58B", "#9BD13B", "#F2B544", "#F07A2F", "#E6465A", "#FD88C0", "#956EFA"];
6
6
  const pipelineColor = z.enum(PIPELINE_COLORS);
7
7
  /** The machine-readable codes the archive route answers with (the API puts `code` in the error body; the client carries it in the error text). */
8
- export const ARCHIVE_ERROR_CODES = "A refusal carries a machine-readable code in the error text: stale_settings_revision (409, settings changed since expectedSettingsRevision was read; nothing moved), target_not_open (409, the destination's first stage is won/lost), dpp_post_close (409, the DPP feed's Post-Close pipeline), archive_too_large (409, too many open deals for one call: move or close some first), target_archived (409, the destination was archived meanwhile), intake_target (409, enabled intake creates its cards in this pipeline), pipeline_archived (409), open_deals (409, open deals but no moveOpenDealsTo), default_pipeline (409), stage_requirements (409, a deal fails the destination's stage-entry rules; nothing moved), archive_raced (409, retry), pipeline_automation_only (403). Archiving a pipeline that is already archived is idempotent: { archived: true, alreadyArchived: true }. The message is the server's, never swallowed. The call can take up to 75 s (a 75 s per-call timeout is used); if it still reports a timeout the archive may have committed: re-read with conduyt_list_pipelines / conduyt_list_archived_pipelines before retrying (a retry of a committed archive answers alreadyArchived, but its open deals have already moved).";
8
+ export const ARCHIVE_ERROR_CODES = "A refusal carries a machine-readable code in the error text: stale_settings_revision (409, settings changed since expectedSettingsRevision was read; nothing moved), target_not_open (409, the destination's first stage is won/lost), dpp_post_close (409, the DPP feed's Post-Close pipeline), archive_too_large (409, too many open deals for one call: move or close some first), target_archived (409, the destination was archived meanwhile), intake_target (409, enabled intake creates its cards in this pipeline), pipeline_archived (409), open_deals (409, open deals but no moveOpenDealsTo), default_pipeline (409), stage_requirements (409, a deal fails the destination's stage-entry rules; nothing moved), archive_raced (409, retry), pipeline_automation_only (403), stage_automation_only (422, moveOpenDealsTo lands in an \"Entry by automation only\" stage; nothing moved). Archiving a pipeline that is already archived is idempotent: { archived: true, alreadyArchived: true }. The message is the server's, never swallowed. The call can take up to 75 s (a 75 s per-call timeout is used); if it still reports a timeout the archive may have committed: re-read with conduyt_list_pipelines / conduyt_list_archived_pipelines before retrying (a retry of a committed archive answers alreadyArchived, but its open deals have already moved).";
9
9
  /** above the archive route's 60 s transaction (up to 500 deal moves) plus post-commit events; the client default is 30 s */
10
10
  export const ARCHIVE_PIPELINE_TIMEOUT_MS = 75_000;
11
11
  export function registerPipelineTools(server, client) {
12
- server.tool("conduyt_list_pipelines", "List all active sales pipelines with their stages (soft-deleted stages are never included). Use this to discover pipeline and stage UUIDs before creating or moving deals. Each pipeline carries color (one of the 10 palette hex colours, or null until the settings page saves one), automationOnly and sortOrder (the board-menu order); an archived pipeline's pre-archive name is archivedFromName (see conduyt_list_archived_pipelines).", {}, async () => {
12
+ server.tool("conduyt_list_pipelines", "List all active sales pipelines with their stages (soft-deleted stages are never included). Use this to discover pipeline and stage UUIDs before creating or moving deals. Each stage carries entryByAutomationOnly (boolean; true = only an automation can move or create a deal INTO it, every other entry is refused 422 stage_automation_only; set it with conduyt_set_stage_rules). Each pipeline carries color (one of the 10 palette hex colours, or null until the settings page saves one), automationOnly and sortOrder (the board-menu order); an archived pipeline's pre-archive name is archivedFromName (see conduyt_list_archived_pipelines).", {}, async () => {
13
13
  const result = await client.get("/api/v1/pipelines");
14
14
  return formatResult(result);
15
15
  });
@@ -107,7 +107,7 @@ export function registerPipelineTools(server, client) {
107
107
  });
108
108
  return formatResult(result);
109
109
  });
110
- server.tool("conduyt_update_stage", "Update a stage within a pipeline — rename, recolor, reorder (sortOrder), or change its won/lost/system flags. A soft-deleted stage (conduyt_delete_stage) answers 404.", {
110
+ server.tool("conduyt_update_stage", "Update a stage within a pipeline — rename, recolor, reorder (sortOrder), or change its won/lost/system flags. A soft-deleted stage (conduyt_delete_stage) answers 404. The \"Entry by automation only\" flag is not set here (the API reads it from the stage rules): use conduyt_set_stage_rules; conduyt_list_pipelines shows it as entryByAutomationOnly on each stage.", {
111
111
  pipelineId: z.string().describe("Pipeline UUID"),
112
112
  stageId: z.string().describe("Stage UUID to update"),
113
113
  name: z.string().optional().describe("Updated stage name"),
@@ -128,7 +128,7 @@ export function registerPipelineTools(server, client) {
128
128
  const result = await client.post(`/api/v1/pipelines/${pipelineId}/stages/reorder`, { order, expectedRevision });
129
129
  return formatResult(result);
130
130
  }));
131
- strictInput(server.tool("conduyt_delete_stage", "Delete a stage from a pipeline (soft delete). Strict input: the guards are spelled moveTo and expectedSettingsRevision, any other spelling (move_to, expected_settings_revision, a typo) is InvalidParams and nothing is deleted. An EMPTY stage can always be deleted, even if deals once used it: the row is kept so deal history, activities, audit rows and reports keep its name, but the stage is hidden from every pipeline and stage list, GET / PATCH on it answer 404, there is NO restore in v1, and its name may be reused by a new stage. Its stage rule, appointment-outcome mappings and status metadata are removed with it. With moveTo (another stage of the same pipeline, same won / lost setting, not a system stage) its live deals move there and the stage is deleted in one transaction. Pass expectedSettingsRevision (the settingsRevision from conduyt_get_settings_revision, read right before) to refuse the delete when settings changed since you read them; the answer is then { id, deleted: true, settingsRevision } with the revision after the delete. A refusal is a 409 whose server message is surfaced verbatim (never swallowed, never retried): deals currently in the stage and no moveTo (\"Cannot delete stage: N deal(s) are in this stage...\"), \"Cannot delete stage: it is used by N active automation(s) (names). Change their stage or pause them first.\" (a published automation references the stage in an action, trigger, condition or wait-until), the DPP status map selects it, plus the coded refusals stale_settings_revision, pipeline_archived and intake_target (enabled intake routes deals into this stage); a moveTo that is not a valid target is a 400. Accepted window: a run parked or in flight before the delete is not tracked; at the step that targets the deleted stage it fails or is skipped with a clear reason in the execution log.", {
131
+ strictInput(server.tool("conduyt_delete_stage", "Delete a stage from a pipeline (soft delete). Strict input: the guards are spelled moveTo and expectedSettingsRevision, any other spelling (move_to, expected_settings_revision, a typo) is InvalidParams and nothing is deleted. An EMPTY stage can always be deleted, even if deals once used it: the row is kept so deal history, activities, audit rows and reports keep its name, but the stage is hidden from every pipeline and stage list, GET / PATCH on it answer 404, there is NO restore in v1, and its name may be reused by a new stage. Its stage rule, appointment-outcome mappings and status metadata are removed with it. With moveTo (another stage of the same pipeline, same won / lost setting, not a system stage) its live deals move there and the stage is deleted in one transaction. Pass expectedSettingsRevision (the settingsRevision from conduyt_get_settings_revision, read right before) to refuse the delete when settings changed since you read them; the answer is then { id, deleted: true, settingsRevision } with the revision after the delete. A refusal is a 409 whose server message is surfaced verbatim (never swallowed, never retried): deals currently in the stage and no moveTo (\"Cannot delete stage: N deal(s) are in this stage...\"), \"Cannot delete stage: it is used by N active automation(s) (names). Change their stage or pause them first.\" (a published automation references the stage in an action, trigger, condition or wait-until), the DPP status map selects it, plus the coded refusals stale_settings_revision, pipeline_archived and intake_target (enabled intake routes deals into this stage); a moveTo that is not a valid target is a 400, and a moveTo stage that is \"Entry by automation only\" is refused 422 stage_automation_only (code and message unchanged). Accepted window: a run parked or in flight before the delete is not tracked; at the step that targets the deleted stage it fails or is skipped with a clear reason in the execution log.", {
132
132
  pipelineId: z.string().describe("Pipeline UUID"),
133
133
  stageId: z.string().describe("Stage UUID to delete"),
134
134
  moveTo: z.string().uuid().optional().describe("Another stage of the same pipeline the live deals move to (sent as move_to)"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt-mcp",
3
- "version": "4.95.0",
3
+ "version": "4.96.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,10 +1,10 @@
1
1
  {
2
- "generatedAt": "2026-10-20T15:03:32.151Z",
2
+ "generatedAt": "2026-10-21T22:15:38.973Z",
3
3
  "toolCount": 359,
4
- "totalEndpoints": 1102,
4
+ "totalEndpoints": 1103,
5
5
  "counts": {
6
6
  "covered": 317,
7
- "excluded": 328,
7
+ "excluded": 329,
8
8
  "planned": 457,
9
9
  "uncategorized": 0
10
10
  },
@@ -317,6 +317,10 @@
317
317
  "status": "excluded",
318
318
  "reason": "super-admin platform operations (Conduyt staff only)"
319
319
  },
320
+ "POST /api/v1/admin/accounts/:id/checkout-sessions/close": {
321
+ "status": "excluded",
322
+ "reason": "super-admin platform operations (Conduyt staff only)"
323
+ },
320
324
  "PATCH /api/v1/admin/accounts/:id/comp": {
321
325
  "status": "excluded",
322
326
  "reason": "super-admin platform operations (Conduyt staff only)"