conduyt-mcp 4.76.0 → 4.77.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 +3 -1
- package/dist/client.d.ts +2 -1
- package/dist/client.js +11 -3
- package/dist/tools/contacts.d.ts +2 -0
- package/dist/tools/contacts.js +26 -4
- package/package.json +1 -1
- package/scripts/coverage-matrix.json +12 -8
package/README.md
CHANGED
|
@@ -17,7 +17,9 @@ MCP server for [Conduyt CRM](https://conduyt.app) — expose your CRM as AI-acce
|
|
|
17
17
|
| `conduyt_list_contacts` | List and search contacts with filters |
|
|
18
18
|
| `conduyt_get_contact` | Get full contact details with timeline |
|
|
19
19
|
| `conduyt_create_contact` | Create a new contact |
|
|
20
|
-
| `conduyt_update_contact` | Update contact fields |
|
|
20
|
+
| `conduyt_update_contact` | Update contact fields; optional `expectedRevision` (0..2147483647) refuses a stale save with 409 `contact_changed` (code + current revision in the error) |
|
|
21
|
+
| `conduyt_add_contact_collaborator` | Add a team member as a collaborator (`POST /contacts/{id}/collaborators`); optional `expectedRevision`, 409 `contact_changed` |
|
|
22
|
+
| `conduyt_remove_contact_collaborator` | Remove a collaborator (`DELETE /contacts/{id}/collaborators`, body); optional `expectedRevision`, 409 `contact_changed`, 404 when not a collaborator |
|
|
21
23
|
| `conduyt_add_tags` | Add tags to a contact |
|
|
22
24
|
|
|
23
25
|
### Deals
|
package/dist/client.d.ts
CHANGED
|
@@ -60,7 +60,8 @@ export declare class ConduytClient {
|
|
|
60
60
|
postText(path: string, body: unknown, opts?: RequestOptions): Promise<string>;
|
|
61
61
|
patch(path: string, body: unknown, opts?: RequestOptions): Promise<unknown>;
|
|
62
62
|
put(path: string, body: unknown, opts?: RequestOptions): Promise<unknown>;
|
|
63
|
-
|
|
63
|
+
/** `body` is for the few DELETE routes that take one (contact collaborators with an expectedRevision pin). */
|
|
64
|
+
del(path: string, opts?: RequestOptions, body?: unknown): Promise<unknown>;
|
|
64
65
|
}
|
|
65
66
|
export declare function formatResult(data: unknown): {
|
|
66
67
|
content: Array<{
|
package/dist/client.js
CHANGED
|
@@ -171,7 +171,14 @@ export class ConduytClient {
|
|
|
171
171
|
// lostReasonOptions) must reach the agent, or a "must be one of the
|
|
172
172
|
// configured options" error is unactionable. Bounded so a pathological
|
|
173
173
|
// body can't bloat the message.
|
|
174
|
-
|
|
174
|
+
// Scalars first (code, revision), nested objects last (a 409 contact_changed carries the whole current
|
|
175
|
+
// `contact`): the bound below may cut a large object, never the code or the current revision.
|
|
176
|
+
const isScalar = (v) => v === null || typeof v !== "object";
|
|
177
|
+
const entries = Object.entries(json ?? {}).filter(([k]) => k !== "error");
|
|
178
|
+
const details = Object.fromEntries([
|
|
179
|
+
...entries.filter(([, v]) => isScalar(v)),
|
|
180
|
+
...entries.filter(([, v]) => !isScalar(v)),
|
|
181
|
+
]);
|
|
175
182
|
if (Object.keys(details).length > 0) {
|
|
176
183
|
msg = `${msg} ${JSON.stringify(details).slice(0, 1000)}`;
|
|
177
184
|
}
|
|
@@ -293,8 +300,9 @@ export class ConduytClient {
|
|
|
293
300
|
put(path, body, opts) {
|
|
294
301
|
return this.request("PUT", path, body, opts);
|
|
295
302
|
}
|
|
296
|
-
|
|
297
|
-
|
|
303
|
+
/** `body` is for the few DELETE routes that take one (contact collaborators with an expectedRevision pin). */
|
|
304
|
+
del(path, opts, body) {
|
|
305
|
+
return this.request("DELETE", path, body, opts);
|
|
298
306
|
}
|
|
299
307
|
}
|
|
300
308
|
export function formatResult(data) {
|
package/dist/tools/contacts.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { ConduytClient } from "../client.js";
|
|
3
|
+
/** The contact save revision pin (catalog bounds: integer 0..2147483647). */
|
|
4
|
+
export declare const CONTACT_REVISION_MAX = 2147483647;
|
|
3
5
|
export declare function registerContactTools(server: McpServer, client: ConduytClient): void;
|
package/dist/tools/contacts.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { formatResult } from "../client.js";
|
|
3
|
+
import { strictInput } from "./strict.js";
|
|
4
|
+
/** The contact save revision pin (catalog bounds: integer 0..2147483647). */
|
|
5
|
+
export const CONTACT_REVISION_MAX = 2147483647;
|
|
6
|
+
const CONTACT_CHANGED = "A stale pin is refused with 409 and code contact_changed (nothing written): the error text carries `code`, the contact's CURRENT `revision` and the current `contact`; reload, decide, and call again with that revision (never retry the same pin). A malformed expectedRevision is a 400; only an omitted field means unpinned.";
|
|
7
|
+
const expectedRevision = () => z.number().int().min(0).max(CONTACT_REVISION_MAX).optional().describe("Optimistic-concurrency pin: the contact `revision` as you last read it (conduyt_get_contact / conduyt_list_contacts / a previous change). Omit for last-write-wins per field.");
|
|
3
8
|
/**
|
|
4
9
|
* Accept BOTH the canonical `customFields` and the REST-style `custom_fields`
|
|
5
10
|
* spelling and return the canonical value to send to the API. The MCP SDK
|
|
@@ -16,7 +21,7 @@ function resolveCustomFields(camel, snake) {
|
|
|
16
21
|
return camel !== undefined ? camel : snake;
|
|
17
22
|
}
|
|
18
23
|
export function registerContactTools(server, client) {
|
|
19
|
-
server.tool("conduyt_list_contacts", "List and search contacts with filtering. Returns paginated results with name, email, phone, tags, source, and assignee.", {
|
|
24
|
+
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).", {
|
|
20
25
|
search: z.string().optional().describe("Search by name, email, or phone number"),
|
|
21
26
|
tag: z.string().optional().describe("Filter by tag name"),
|
|
22
27
|
source: z.string().optional().describe("Filter by source (e.g. 'signup', 'import', 'manual')"),
|
|
@@ -46,7 +51,7 @@ export function registerContactTools(server, client) {
|
|
|
46
51
|
const result = await client.get(`/api/v1/contacts?${params}`);
|
|
47
52
|
return formatResult(result);
|
|
48
53
|
});
|
|
49
|
-
server.tool("conduyt_get_contact", "Get a single contact with full details — profile, tags, custom fields, timeline, deals, and activity history.", {
|
|
54
|
+
server.tool("conduyt_get_contact", "Get a single contact with full details — profile, tags, custom fields, timeline, deals, and activity history. Includes `revision` (integer save revision): pass it as expectedRevision on conduyt_update_contact or the collaborator tools so a newer change is never overwritten.", {
|
|
50
55
|
id: z.string().describe("Contact UUID"),
|
|
51
56
|
}, async ({ id }) => {
|
|
52
57
|
const result = await client.get(`/api/v1/contacts/${id}`);
|
|
@@ -70,7 +75,7 @@ export function registerContactTools(server, client) {
|
|
|
70
75
|
const result = await client.post("/api/v1/contacts", body);
|
|
71
76
|
return formatResult(result);
|
|
72
77
|
});
|
|
73
|
-
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).", {
|
|
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, {
|
|
74
79
|
id: z.string().describe("Contact UUID"),
|
|
75
80
|
email: z.string().optional().describe("Updated email"),
|
|
76
81
|
firstName: z.string().optional().describe("Updated first name"),
|
|
@@ -82,12 +87,29 @@ export function registerContactTools(server, client) {
|
|
|
82
87
|
masterStatus: z.string().trim().min(1).max(40).optional().describe("Lifecycle status of the LEAD (master record), distinct from any deal's status: 'open' (default), 'won', 'lost', 'abandoned', 'disqualified', or an account-defined custom status. A terminal status (won/lost/abandoned/disqualified) SILENCES all outbound automations for this contact — drips, sequences, and scheduled sends stop. Use conduyt_get_lifecycle_settings to see the account's configured statuses."),
|
|
83
88
|
customFields: z.record(z.any()).optional().describe("Structured custom-field values to merge, keyed by field key. Omit a key to leave it unchanged."),
|
|
84
89
|
custom_fields: z.record(z.any()).optional().describe("Alias for customFields (snake_case). Provide only one of customFields / custom_fields."),
|
|
90
|
+
expectedRevision: expectedRevision(),
|
|
85
91
|
}, async ({ id, customFields, custom_fields, ...updates }) => {
|
|
86
92
|
const cf = resolveCustomFields(customFields, custom_fields);
|
|
87
93
|
const body = cf !== undefined ? { ...updates, customFields: cf } : updates;
|
|
88
94
|
const result = await client.patch(`/api/v1/contacts/${id}`, body);
|
|
89
95
|
return formatResult(result);
|
|
90
|
-
});
|
|
96
|
+
}));
|
|
97
|
+
strictInput(server.tool("conduyt_add_contact_collaborator", "Add a team member as a collaborator on a contact (POST /api/v1/contacts/{id}/collaborators; needs edit authority over the contact). Answers { added, revision } where revision is the contact's NEW revision. Pass expectedRevision to refuse the change when the contact changed since you read it. " + CONTACT_CHANGED + " 409 without a code: the user is already a collaborator; 404: no such contact for you or no edit authority; 413: body over 16 KB.", {
|
|
98
|
+
contactId: z.string().uuid().describe("Contact UUID"),
|
|
99
|
+
userId: z.string().min(1).describe("The team member to add"),
|
|
100
|
+
expectedRevision: expectedRevision(),
|
|
101
|
+
}, async ({ contactId, userId, expectedRevision }) => {
|
|
102
|
+
const result = await client.post(`/api/v1/contacts/${encodeURIComponent(contactId)}/collaborators`, { userId, expectedRevision });
|
|
103
|
+
return formatResult(result);
|
|
104
|
+
}));
|
|
105
|
+
strictInput(server.tool("conduyt_remove_contact_collaborator", "Remove a collaborator from a contact (DELETE /api/v1/contacts/{id}/collaborators with a body; needs edit authority). Answers { removed, revision } where revision is the contact's NEW revision. Pass expectedRevision to refuse the change when the contact changed since you read it. " + CONTACT_CHANGED + " 404 also when the user is not a collaborator of the contact; 409 without a code: the user is still the assignee of a deal or hosts an appointment on this contact; 413: body over 16 KB.", {
|
|
106
|
+
contactId: z.string().uuid().describe("Contact UUID"),
|
|
107
|
+
userId: z.string().min(1).describe("The team member to remove"),
|
|
108
|
+
expectedRevision: expectedRevision(),
|
|
109
|
+
}, async ({ contactId, userId, expectedRevision }) => {
|
|
110
|
+
const result = await client.del(`/api/v1/contacts/${encodeURIComponent(contactId)}/collaborators`, undefined, { userId, expectedRevision });
|
|
111
|
+
return formatResult(result);
|
|
112
|
+
}));
|
|
91
113
|
server.tool("conduyt_delete_contact", "Delete a contact by UUID (soft-delete — the contact is removed from lists/searches but the row is retained for audit, and its deals are unaffected). Use this to clean up test or duplicate contacts. There is no undelete endpoint, so treat it as irreversible via the API.", {
|
|
92
114
|
// .uuid() so a malformed id with slashes can't form a different DELETE
|
|
93
115
|
// subroute (e.g. /contacts/:id/tags/:tagId); encode the segment too.
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-10-
|
|
3
|
-
"toolCount":
|
|
2
|
+
"generatedAt": "2026-10-09T02:56:11.306Z",
|
|
3
|
+
"toolCount": 330,
|
|
4
4
|
"totalEndpoints": 1064,
|
|
5
5
|
"counts": {
|
|
6
|
-
"covered":
|
|
6
|
+
"covered": 296,
|
|
7
7
|
"excluded": 318,
|
|
8
|
-
"planned":
|
|
8
|
+
"planned": 450,
|
|
9
9
|
"uncategorized": 0
|
|
10
10
|
},
|
|
11
11
|
"matrix": {
|
|
@@ -1450,12 +1450,16 @@
|
|
|
1450
1450
|
"reason": "contact subroutes beyond the core CRUD/search tools"
|
|
1451
1451
|
},
|
|
1452
1452
|
"POST /api/v1/contacts/:id/collaborators": {
|
|
1453
|
-
"status": "
|
|
1454
|
-
"
|
|
1453
|
+
"status": "covered",
|
|
1454
|
+
"tools": [
|
|
1455
|
+
"conduyt_add_contact_collaborator"
|
|
1456
|
+
]
|
|
1455
1457
|
},
|
|
1456
1458
|
"DELETE /api/v1/contacts/:id/collaborators": {
|
|
1457
|
-
"status": "
|
|
1458
|
-
"
|
|
1459
|
+
"status": "covered",
|
|
1460
|
+
"tools": [
|
|
1461
|
+
"conduyt_remove_contact_collaborator"
|
|
1462
|
+
]
|
|
1459
1463
|
},
|
|
1460
1464
|
"GET /api/v1/contacts/:id/dnc": {
|
|
1461
1465
|
"status": "planned",
|