conduyt-mcp 4.79.0 → 4.80.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 +20 -0
- package/dist/client.d.ts +14 -0
- package/dist/client.js +57 -24
- package/dist/tools/deals.js +29 -0
- package/dist/tools/form-intakes.d.ts +2 -1
- package/dist/tools/form-intakes.js +4 -3
- package/dist/tools/tasks.js +2 -2
- package/package.json +1 -1
- package/scripts/coverage-matrix.json +4 -2
package/README.md
CHANGED
|
@@ -27,6 +27,8 @@ MCP server for [Conduyt CRM](https://conduyt.app) — expose your CRM as AI-acce
|
|
|
27
27
|
|------|-------------|
|
|
28
28
|
| `conduyt_list_deals` | List deals by pipeline, stage, or assignee; rows carry `hasNextStep` and `nextAppointmentAt`. With `health_summary=true` it answers the Pipeline health strip for the same filtered set (`totalOpenDeals`, `atRiskValue`, `checks[]`, `deals`); `health_check`, `health_limit`, `health_offset`, `health_cursor` (the previous `nextCursor`, passed back unchanged) and `health_rows_only` page one check's deals (rows-only answers `{data:{deals:{check, offset, items, hasMore, nextCursor}}}`) |
|
|
29
29
|
| `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` |
|
|
30
|
+
| `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` |
|
|
31
|
+
| `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 |
|
|
30
32
|
| `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 |
|
|
31
33
|
| `conduyt_get_deal_card_fields` / `conduyt_set_deal_card_fields` | Read / replace the kanban card fields (1-8, built-ins or `custom:<fieldKey>`) |
|
|
32
34
|
| `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` |
|
|
@@ -342,6 +344,24 @@ The rest of team chat (sending, reactions, pins, presence, uploads) is not wrapp
|
|
|
342
344
|
| `conduyt_reply_capture_verify` | Check DNS and promote to live once the provider confirms inbound receiving |
|
|
343
345
|
| `conduyt_reply_capture_remove` | Remove capture (and finish an interrupted removal) — requires `confirm='REMOVE'`; replies to already-sent email are lost |
|
|
344
346
|
|
|
347
|
+
## Error text grammar
|
|
348
|
+
|
|
349
|
+
A refused call (HTTP 4xx/5xx) fails the tool with this text, stable across tools:
|
|
350
|
+
|
|
351
|
+
```
|
|
352
|
+
Conduyt API <status>: <server message>[ [Retry-After: <N>s]][ <details>]
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
`[Retry-After: Ns]` appears only when the server sent the header (shown, never acted on: a mutation is never retried).
|
|
356
|
+
`<details>` appears only when the body had more than `error`: it is ONE JSON object and the FINAL token. To extract it, try
|
|
357
|
+
each `{` from the left until the rest of the text parses as JSON (a server message may itself contain a brace). The object
|
|
358
|
+
carries scalars first (`code`, revision), then nested objects whole; `HealthFixRefusal.row` is always whole, any other nested
|
|
359
|
+
object too large for the bound is left out and named in `_omitted`. Every HTTP refusal takes this path, including the multipart call-flow audio upload; a tool that adds its own words (a missing-scope or busy note) puts them BEFORE this text. Example:
|
|
360
|
+
|
|
361
|
+
```
|
|
362
|
+
Conduyt API 503: Contact merge in progress [Retry-After: 5s] {"code":"merge_in_progress"}
|
|
363
|
+
```
|
|
364
|
+
|
|
345
365
|
## Setup
|
|
346
366
|
|
|
347
367
|
```bash
|
package/dist/client.d.ts
CHANGED
|
@@ -28,7 +28,15 @@ export declare class ConduytApiError extends Error {
|
|
|
28
28
|
export interface RequestOptions {
|
|
29
29
|
timeoutMs?: number;
|
|
30
30
|
maxResponseBytes?: number;
|
|
31
|
+
/** Extra request headers (e.g. Idempotency-Key on POST /tasks); never overrides Authorization / Content-Type. */
|
|
32
|
+
headers?: Record<string, string>;
|
|
31
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Render an error body's details (everything but `error`) as ONE valid JSON object: scalars first (code, revision; a very
|
|
36
|
+
* long string is shortened inside its quotes), then nested objects whole while they fit ERROR_DETAILS_BOUND, plus the
|
|
37
|
+
* ALWAYS_WHOLE_KEYS; a nested object that does not fit is left out and named in `_omitted`. JSON is never cut mid-object.
|
|
38
|
+
*/
|
|
39
|
+
export declare function renderErrorDetails(body: Record<string, unknown>): string;
|
|
32
40
|
export declare class ConduytClient {
|
|
33
41
|
private baseUrl;
|
|
34
42
|
private apiKey;
|
|
@@ -42,7 +50,13 @@ export declare class ConduytClient {
|
|
|
42
50
|
* after (chunked responses carry no length header).
|
|
43
51
|
*/
|
|
44
52
|
private attempt;
|
|
53
|
+
/**
|
|
54
|
+
* Error grammar (stable): `Conduyt API <status>: <message>` + optionally ` [Retry-After: <N>s]` + optionally ` <details>`,
|
|
55
|
+
* where <details> is ONE JSON object and the FINAL token (see renderErrorDetails). Parse it by trying each `{` from the left
|
|
56
|
+
* until the rest of the text is valid JSON (a server message may itself contain a brace).
|
|
57
|
+
*/
|
|
45
58
|
private httpError;
|
|
59
|
+
private mkHttpError;
|
|
46
60
|
private retryDelayMs;
|
|
47
61
|
/**
|
|
48
62
|
* Retries are for idempotent GETs ONLY: transient statuses (429/502/503/
|
package/dist/client.js
CHANGED
|
@@ -32,6 +32,35 @@ const MAX_RETRY_AFTER_MS = 10_000;
|
|
|
32
32
|
const isAbort = (err) => err?.name === "TimeoutError" || err?.name === "AbortError";
|
|
33
33
|
/** what a timeout of a MUTATION means: the server may have applied it */
|
|
34
34
|
const AMBIGUOUS = " The server may have applied it: read the current state before repeating it.";
|
|
35
|
+
/** Room for the nested objects of an error body (a 409 contact_changed's `contact`, a stage-requirements list). */
|
|
36
|
+
const ERROR_DETAILS_BOUND = 1000;
|
|
37
|
+
/** Error-body keys that are the actionable state itself and are always rendered WHOLE: HealthFixRefusal.row (the deal's
|
|
38
|
+
* current health row, which a caller needs to choose again). The response size cap bounds it. */
|
|
39
|
+
const ALWAYS_WHOLE_KEYS = new Set(["row"]);
|
|
40
|
+
/**
|
|
41
|
+
* Render an error body's details (everything but `error`) as ONE valid JSON object: scalars first (code, revision; a very
|
|
42
|
+
* long string is shortened inside its quotes), then nested objects whole while they fit ERROR_DETAILS_BOUND, plus the
|
|
43
|
+
* ALWAYS_WHOLE_KEYS; a nested object that does not fit is left out and named in `_omitted`. JSON is never cut mid-object.
|
|
44
|
+
*/
|
|
45
|
+
export function renderErrorDetails(body) {
|
|
46
|
+
const entries = Object.entries(body).filter(([k]) => k !== "error");
|
|
47
|
+
if (entries.length === 0)
|
|
48
|
+
return "";
|
|
49
|
+
const isScalar = (v) => v === null || typeof v !== "object";
|
|
50
|
+
const out = {};
|
|
51
|
+
for (const [k, v] of entries.filter(([, v]) => isScalar(v)))
|
|
52
|
+
out[k] = typeof v === "string" && v.length > ERROR_DETAILS_BOUND ? v.slice(0, ERROR_DETAILS_BOUND) + "...[shortened]" : v;
|
|
53
|
+
const omitted = [];
|
|
54
|
+
for (const [k, v] of entries.filter(([, v]) => !isScalar(v))) {
|
|
55
|
+
if (ALWAYS_WHOLE_KEYS.has(k) || JSON.stringify({ ...out, [k]: v }).length <= ERROR_DETAILS_BOUND)
|
|
56
|
+
out[k] = v;
|
|
57
|
+
else
|
|
58
|
+
omitted.push(k);
|
|
59
|
+
}
|
|
60
|
+
if (omitted.length > 0)
|
|
61
|
+
out._omitted = omitted;
|
|
62
|
+
return JSON.stringify(out);
|
|
63
|
+
}
|
|
35
64
|
export class ConduytClient {
|
|
36
65
|
baseUrl;
|
|
37
66
|
apiKey;
|
|
@@ -63,9 +92,10 @@ export class ConduytClient {
|
|
|
63
92
|
* from Content-Length before the body is read AND from the actual bytes
|
|
64
93
|
* after (chunked responses carry no length header).
|
|
65
94
|
*/
|
|
66
|
-
async attempt(method, path, body, timeoutMs, maxResponseBytes) {
|
|
95
|
+
async attempt(method, path, body, timeoutMs, maxResponseBytes, extraHeaders) {
|
|
67
96
|
const url = `${this.baseUrl}${path}`;
|
|
68
97
|
const headers = {
|
|
98
|
+
...(extraHeaders ?? {}),
|
|
69
99
|
Authorization: `Bearer ${this.apiKey}`,
|
|
70
100
|
"Content-Type": "application/json",
|
|
71
101
|
};
|
|
@@ -161,36 +191,40 @@ export class ConduytClient {
|
|
|
161
191
|
}
|
|
162
192
|
return { res, text };
|
|
163
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Error grammar (stable): `Conduyt API <status>: <message>` + optionally ` [Retry-After: <N>s]` + optionally ` <details>`,
|
|
196
|
+
* where <details> is ONE JSON object and the FINAL token (see renderErrorDetails). Parse it by trying each `{` from the left
|
|
197
|
+
* until the rest of the text is valid JSON (a server message may itself contain a brace).
|
|
198
|
+
*/
|
|
164
199
|
httpError(res, text) {
|
|
200
|
+
const retryAfterRaw = res.headers.get("retry-after");
|
|
201
|
+
const retryAfter = retryAfterRaw === null || retryAfterRaw.trim() === "" ? NaN : Number(retryAfterRaw);
|
|
202
|
+
const retryAfterOk = Number.isFinite(retryAfter) && retryAfter >= 0;
|
|
203
|
+
const raTag = retryAfterOk ? ` [Retry-After: ${retryAfter}s]` : "";
|
|
165
204
|
let msg = res.statusText;
|
|
166
205
|
try {
|
|
167
206
|
const json = JSON.parse(text);
|
|
168
207
|
if (json?.error)
|
|
169
208
|
msg = json.error;
|
|
170
|
-
// Structured error details (e.g. STAGE_REQUIREMENTS carries
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const details = Object.fromEntries([
|
|
179
|
-
...entries.filter(([, v]) => isScalar(v)),
|
|
180
|
-
...entries.filter(([, v]) => !isScalar(v)),
|
|
181
|
-
]);
|
|
182
|
-
if (Object.keys(details).length > 0) {
|
|
183
|
-
msg = `${msg} ${JSON.stringify(details).slice(0, 1000)}`;
|
|
184
|
-
}
|
|
209
|
+
// Structured error details (e.g. STAGE_REQUIREMENTS carries lostReasonOptions) must reach the agent, or the
|
|
210
|
+
// refusal is unactionable. Rendered by renderErrorDetails: JSON is never cut mid-object.
|
|
211
|
+
const details = renderErrorDetails(json ?? {});
|
|
212
|
+
if (details)
|
|
213
|
+
msg = `${msg}${raTag} ${details}`;
|
|
214
|
+
else
|
|
215
|
+
msg = `${msg}${raTag}`;
|
|
216
|
+
return this.mkHttpError(res, `Conduyt API ${res.status}: ${msg}`, retryAfter, retryAfterOk);
|
|
185
217
|
}
|
|
186
218
|
catch {
|
|
187
219
|
if (text)
|
|
188
220
|
msg = text.slice(0, 500);
|
|
221
|
+
msg = `${msg}${raTag}`;
|
|
189
222
|
}
|
|
190
|
-
|
|
191
|
-
|
|
223
|
+
return this.mkHttpError(res, `Conduyt API ${res.status}: ${msg}`, retryAfter, retryAfterOk);
|
|
224
|
+
}
|
|
225
|
+
mkHttpError(res, message, retryAfter, retryAfterOk) {
|
|
192
226
|
return new ConduytApiError({
|
|
193
|
-
message
|
|
227
|
+
message,
|
|
194
228
|
status: res.status,
|
|
195
229
|
code: "http",
|
|
196
230
|
retryable: RETRYABLE_STATUSES.has(res.status),
|
|
@@ -224,7 +258,7 @@ export class ConduytClient {
|
|
|
224
258
|
const remaining = totalBudgetMs - (Date.now() - startedAt);
|
|
225
259
|
let res = null;
|
|
226
260
|
try {
|
|
227
|
-
const out = await this.attempt(method, path, body, Math.max(1, Math.min(timeoutMs, remaining)), maxResponseBytes);
|
|
261
|
+
const out = await this.attempt(method, path, body, Math.max(1, Math.min(timeoutMs, remaining)), maxResponseBytes, opts?.headers);
|
|
228
262
|
res = out.res;
|
|
229
263
|
if (out.res.ok)
|
|
230
264
|
return out.text;
|
|
@@ -282,10 +316,9 @@ export class ConduytClient {
|
|
|
282
316
|
catch {
|
|
283
317
|
json = { raw: text };
|
|
284
318
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
throw
|
|
288
|
-
}
|
|
319
|
+
// every non-2xx goes through the shared formatter: same grammar, Retry-After placement and final JSON details object
|
|
320
|
+
if (!res.ok)
|
|
321
|
+
throw this.httpError(res, text);
|
|
289
322
|
return json;
|
|
290
323
|
}
|
|
291
324
|
post(path, body, opts) {
|
package/dist/tools/deals.js
CHANGED
|
@@ -112,6 +112,35 @@ export function registerDealTools(server, client) {
|
|
|
112
112
|
const result = qs ? await client.get("/api/v1/deals/inspection?" + qs) : await client.get("/api/v1/deals/inspection");
|
|
113
113
|
return formatResult(result);
|
|
114
114
|
}));
|
|
115
|
+
strictInput(server.tool("conduyt_pipeline_health_fix_deal", "Pipeline health QUICK FIX for a deal row (PATCH /api/v1/deals/{id} with checkOrigin). Pass the health check the row came from, the deal's updatedAt exactly as the health row carried it (expectedUpdatedAt, REQUIRED), and ONLY that check's fix: stale = stageId (+ optional expectedStageId, the stage the move was chosen against); unassigned = assignedTo; missing_close_date = expectedCloseDate; missing_contact = contactId. Each set to a value; any other combination is refused 400 health_fix_mismatch and nothing is read or written. For the next-action checks (high_value_idle, needs_action) use conduyt_pipeline_health_fix_next_action instead. Refusals reach you verbatim (status, server message, code, and the deal's CURRENT health row) and are NEVER retried. Error text grammar: `Conduyt API <status>: <message>[ [Retry-After: Ns]][ <one JSON object, the final token>]` (see README, Error text grammar). 409 deal_changed (the deal is no longer at expectedUpdatedAt; error text carries updatedAt and the current row: read it, choose again against row.updatedAt), 409 deal_check_cleared (the deal no longer fails that check; nothing written), a 409 without a code (a stage move by another writer, a stale expectedStageId, an in-flight publication), 400 health_fix_mismatch, 404 (no such deal, or your key cannot see it). The row in a refusal is null when your key can no longer see the deal. Ordinary edits (several fields, no health context) stay on conduyt_update_deal.", {
|
|
116
|
+
dealId: z.string().uuid().describe("Deal UUID"),
|
|
117
|
+
checkOrigin: z.enum(["stale", "unassigned", "missing_close_date", "missing_contact"]).describe("The health check this edit fixes"),
|
|
118
|
+
expectedUpdatedAt: z.string().datetime({ message: "expectedUpdatedAt must be the deal's updatedAt exactly as the health row carried it (UTC, trailing Z, e.g. 2026-10-06T23:19:00.000Z)" }).describe("REQUIRED: the deal's updatedAt as the health row carried it (PipelineHealthDeal.updatedAt). A deal at another version is refused 409 deal_changed."),
|
|
119
|
+
stageId: z.string().uuid().optional().describe("stale only: the stage to move the deal to"),
|
|
120
|
+
expectedStageId: z.string().uuid().optional().describe("stale only: the stage the move was chosen against; refused 409 (nothing written) when the deal is no longer in it"),
|
|
121
|
+
assignedTo: z.string().uuid().optional().describe("unassigned only: the user (owner) UUID to assign"),
|
|
122
|
+
expectedCloseDate: z.string().optional().describe("missing_close_date only: the expected close date (ISO 8601, e.g. '2026-11-30')"),
|
|
123
|
+
contactId: z.string().uuid().optional().describe("missing_contact only: the contact UUID to link"),
|
|
124
|
+
}, async ({ dealId, checkOrigin, expectedUpdatedAt, stageId, expectedStageId, assignedTo, expectedCloseDate, contactId }) => {
|
|
125
|
+
const result = await client.patch(`/api/v1/deals/${dealId}`, { checkOrigin, expectedUpdatedAt, stageId, expectedStageId, assignedTo, expectedCloseDate, contactId });
|
|
126
|
+
return formatResult(result);
|
|
127
|
+
}));
|
|
128
|
+
strictInput(server.tool("conduyt_pipeline_health_fix_next_action", "Pipeline health QUICK FIX \"Set next action\" (POST /api/v1/tasks with checkOrigin): creates a follow-up task on a deal from the High-value idle or Needs next action row. Pass checkOrigin, dealId and expectedDealUpdatedAt (the deal's updatedAt as the health row carried it); the task is created only while, under the deal's next-action lock, the deal is still at that version, still has no next action (no open task, no appointment booked from a day ago on) and still fails the check. Send idempotencyKey (1-255 printable ASCII) so a retry after a lost response replays the first task; reuse it ONLY for the same request. Refusals reach you verbatim (status, message, code, the deal's CURRENT health row) and are NEVER retried. Error text grammar: `Conduyt API <status>: <message>[ [Retry-After: Ns]][ <one JSON object, the final token>]` (see README, Error text grammar). 409 deal_changed (read the row, choose again against row.updatedAt), 409 deal_has_next_action (This deal already has a next action booked.), 409 deal_check_cleared, 400 health_fix_mismatch (checkOrigin not high_value_idle / needs_action, or without dealId / expectedDealUpdatedAt), a 409 without a code (the contact was merged or deleted mid-request: retry against the surviving record), or an idempotency 409/503 (see the idempotencyKey notes; verify the outcome before using a new key). The database allows at most one non-done quick-fix task per deal (a second is refused 409 deal_has_next_action); ordinary tasks (conduyt_create_task) are unconstrained and may coexist with or race a quick fix.", {
|
|
129
|
+
checkOrigin: z.enum(["high_value_idle", "needs_action"]).describe("The health check this task fixes"),
|
|
130
|
+
dealId: z.string().uuid().describe("Deal UUID the task is for"),
|
|
131
|
+
expectedDealUpdatedAt: z.string().datetime({ message: "expectedDealUpdatedAt must be the deal's updatedAt exactly as the health row carried it (UTC, trailing Z)" }).describe("REQUIRED: the deal's updatedAt as the health row carried it (PipelineHealthDeal.updatedAt)"),
|
|
132
|
+
title: z.string().min(1).max(500).describe("Task title (trimmed; blank is refused)"),
|
|
133
|
+
description: z.string().max(5000).optional().describe("Task description (max 5000)"),
|
|
134
|
+
dueDate: z.string().optional().describe("Due date: a date-only YYYY-MM-DD is stored as noon UTC that day; a date-time as given"),
|
|
135
|
+
priority: z.enum(["low", "medium", "high", "urgent"]).optional().describe("Task priority (default medium)"),
|
|
136
|
+
status: z.enum(["todo", "in_progress", "done"]).optional().describe("Task status (default todo)"),
|
|
137
|
+
assignedTo: z.string().nullable().optional().describe("A member's user UUID, '@me' or '@owner'; null or omitted = unassigned (assignment rules may still assign it). Assigning to someone else needs tasks.assign."),
|
|
138
|
+
contactId: z.string().uuid().nullable().optional().describe("Contact UUID to link"),
|
|
139
|
+
idempotencyKey: z.string().regex(/^[!-~](?:[ -~]{0,253}[!-~])?$/, "idempotencyKey must be 1-255 printable ASCII characters, not blank").optional().describe("Sent as the Idempotency-Key header: one key per creation attempt, reused only to retry the SAME request (same key + same body replays the first task; same key + a different body is 422). Two distinct 409s: the original attempt is still processing (wait Retry-After, retry with the SAME key), or code idempotency_key_spent (a prior attempt failed or has an unknown outcome: verify whether it took effect BEFORE a new key). 503 idempotency_claim_held: retry with a NEW key; 503 idempotency_outcome_unknown: verify the outcome first."),
|
|
140
|
+
}, async ({ idempotencyKey, checkOrigin, dealId, expectedDealUpdatedAt, title, description, dueDate, priority, status, assignedTo, contactId }) => {
|
|
141
|
+
const result = await client.post("/api/v1/tasks", { checkOrigin, dealId, expectedDealUpdatedAt, title, description, dueDate, priority, status, assignedTo, contactId }, idempotencyKey !== undefined ? { headers: { "Idempotency-Key": idempotencyKey } } : undefined);
|
|
142
|
+
return formatResult(result);
|
|
143
|
+
}));
|
|
115
144
|
server.tool("conduyt_get_deal", "Get a single deal with full details — value, currency, probability, stage, contact, expected close date, custom-field values, activities, tasks, and history. Use this to read structured custom-field data back out of a deal (e.g. for a P&L or balance-sheet model). Deal rows also carry hasNextStep and nextAppointmentAt (null unless your key can view the calendar) when the API returns them.", {
|
|
116
145
|
id: z.string().describe("Deal UUID"),
|
|
117
146
|
}, async ({ id }) => {
|
|
@@ -11,7 +11,8 @@ import { ConduytClient } from "../client.js";
|
|
|
11
11
|
/** What each tool's key must hold: the API names only the FIRST missing scope, so a 403 is surfaced with all of it. */
|
|
12
12
|
export declare const LIST_AND_CREATE_REQUIREMENT = "the form-intakes AND contacts scopes on a write or admin tier key (a member: settings.edit, contacts.create and contacts.edit)";
|
|
13
13
|
export declare const REVOKE_REQUIREMENT = "the form-intakes scope on an admin tier key (a member: settings.edit)";
|
|
14
|
-
/** A 403 names the complete requirement; a busy 503 says when to retry (never replayed here: create is a mutation).
|
|
14
|
+
/** A 403 names the complete requirement; a busy 503 says when to retry (never replayed here: create is a mutation). Annotations go BEFORE the
|
|
15
|
+
* client's message, so its details JSON object stays the final token (README, Error text grammar). */
|
|
15
16
|
export declare function intakeToolError(err: unknown, requirement: string): {
|
|
16
17
|
content: Array<{
|
|
17
18
|
type: "text";
|
|
@@ -11,13 +11,14 @@ import { ConduytApiError, formatResult, formatToolError } from "../client.js";
|
|
|
11
11
|
/** What each tool's key must hold: the API names only the FIRST missing scope, so a 403 is surfaced with all of it. */
|
|
12
12
|
export const LIST_AND_CREATE_REQUIREMENT = "the form-intakes AND contacts scopes on a write or admin tier key (a member: settings.edit, contacts.create and contacts.edit)";
|
|
13
13
|
export const REVOKE_REQUIREMENT = "the form-intakes scope on an admin tier key (a member: settings.edit)";
|
|
14
|
-
/** A 403 names the complete requirement; a busy 503 says when to retry (never replayed here: create is a mutation).
|
|
14
|
+
/** A 403 names the complete requirement; a busy 503 says when to retry (never replayed here: create is a mutation). Annotations go BEFORE the
|
|
15
|
+
* client's message, so its details JSON object stays the final token (README, Error text grammar). */
|
|
15
16
|
export function intakeToolError(err, requirement) {
|
|
16
17
|
if (err instanceof ConduytApiError) {
|
|
17
18
|
if (err.status === 403)
|
|
18
|
-
return formatToolError(
|
|
19
|
+
return formatToolError(`This tool needs ${requirement}. ${err.message}`);
|
|
19
20
|
if (err.status === 503 && err.retryAfterSeconds !== null) {
|
|
20
|
-
return formatToolError(`busy; retry after ${err.retryAfterSeconds} s
|
|
21
|
+
return formatToolError(`busy; retry after ${err.retryAfterSeconds} s: ${err.message}`);
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
throw err;
|
package/dist/tools/tasks.js
CHANGED
|
@@ -49,7 +49,7 @@ export function registerTaskTools(server, client) {
|
|
|
49
49
|
const result = await client.get(`/api/v1/tasks?${params}`);
|
|
50
50
|
return formatResult(result);
|
|
51
51
|
});
|
|
52
|
-
server.tool("conduyt_create_task", "Create a new task. Can be linked to a contact or deal and assigned to a team member.", {
|
|
52
|
+
server.tool("conduyt_create_task", "Create a new task. Can be linked to a contact or deal and assigned to a team member. This is the ORDINARY task route: ordinary tasks are unconstrained and may coexist with or race a Pipeline health quick-fix task. The database allows at most one non-done quick-fix task per deal (a second is refused 409 deal_has_next_action); to create the quick fix itself (checkOrigin + expectedDealUpdatedAt, guarded against a deal that changed) use conduyt_pipeline_health_fix_next_action. A 409 without a code means the contact was merged or deleted mid-request: retry against the surviving record. Errors reach you verbatim and are never retried.", {
|
|
53
53
|
title: z.string().describe("Task title"),
|
|
54
54
|
description: z.string().optional().describe("Task description"),
|
|
55
55
|
dueDate: z.string().optional().describe("Due date (ISO 8601)"),
|
|
@@ -68,7 +68,7 @@ export function registerTaskTools(server, client) {
|
|
|
68
68
|
const result = await client.get(`/api/v1/tasks/${encodeURIComponent(id)}`);
|
|
69
69
|
return formatResult(result);
|
|
70
70
|
});
|
|
71
|
-
server.tool("conduyt_update_task", "Update an existing task's fields. Only the fields you provide are changed; omit a field to leave it as-is. To mark a task done, prefer conduyt_complete_task. Requires tasks:edit.", {
|
|
71
|
+
server.tool("conduyt_update_task", "Update an existing task's fields. Only the fields you provide are changed; omit a field to leave it as-is. To mark a task done, prefer conduyt_complete_task. Requires tasks:edit. A 409 means nothing was written, surfaced verbatim and never retried: code deal_has_next_action (\"This deal already has a next action booked.\": re-opening or re-linking a quick-fix task would give the deal a second open one), or no code (the task changed while saving: read it again and retry).", {
|
|
72
72
|
id: z.string().uuid().describe("Task UUID to update"),
|
|
73
73
|
title: z.string().optional().describe("Updated task title"),
|
|
74
74
|
description: z.string().optional().describe("Updated description (pass an empty string to clear it)"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-10-
|
|
3
|
-
"toolCount":
|
|
2
|
+
"generatedAt": "2026-10-09T06:41:49.430Z",
|
|
3
|
+
"toolCount": 336,
|
|
4
4
|
"totalEndpoints": 1066,
|
|
5
5
|
"counts": {
|
|
6
6
|
"covered": 299,
|
|
@@ -2165,6 +2165,7 @@
|
|
|
2165
2165
|
"PATCH /api/v1/deals/:id": {
|
|
2166
2166
|
"status": "covered",
|
|
2167
2167
|
"tools": [
|
|
2168
|
+
"conduyt_pipeline_health_fix_deal",
|
|
2168
2169
|
"conduyt_update_deal"
|
|
2169
2170
|
]
|
|
2170
2171
|
},
|
|
@@ -4495,6 +4496,7 @@
|
|
|
4495
4496
|
"POST /api/v1/tasks": {
|
|
4496
4497
|
"status": "covered",
|
|
4497
4498
|
"tools": [
|
|
4499
|
+
"conduyt_pipeline_health_fix_next_action",
|
|
4498
4500
|
"conduyt_create_task"
|
|
4499
4501
|
]
|
|
4500
4502
|
},
|