conduyt-mcp 4.3.2 → 4.4.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/index.js +1 -1
- package/dist/tools/contacts.js +36 -5
- package/dist/tools/deals.js +45 -8
- package/package.json +1 -1
- package/dist/tools/knowledge.d.ts +0 -14
- package/dist/tools/knowledge.js +0 -38
package/dist/index.js
CHANGED
|
@@ -45,7 +45,7 @@ if (!apiKey.startsWith("cdy_")) {
|
|
|
45
45
|
const client = new ConduytClient({ apiUrl, apiKey });
|
|
46
46
|
const server = new McpServer({
|
|
47
47
|
name: "conduyt",
|
|
48
|
-
version: "4.3.
|
|
48
|
+
version: "4.3.3",
|
|
49
49
|
});
|
|
50
50
|
registerDiscoveryTools(server, client);
|
|
51
51
|
registerContactTools(server, client);
|
package/dist/tools/contacts.js
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { formatResult } from "../client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Accept BOTH the canonical `customFields` and the REST-style `custom_fields`
|
|
5
|
+
* spelling and return the canonical value to send to the API. The MCP SDK
|
|
6
|
+
* strips schema-undeclared keys, so a caller using the snake_case alias (which
|
|
7
|
+
* is plausible — this file also exposes per_page / assigned_to) would otherwise
|
|
8
|
+
* have their custom-field values silently dropped. Declaring both and
|
|
9
|
+
* normalizing here closes that data-loss path; supplying both at once is a
|
|
10
|
+
* conflict we reject rather than silently pick one.
|
|
11
|
+
*/
|
|
12
|
+
function resolveCustomFields(camel, snake) {
|
|
13
|
+
if (camel !== undefined && snake !== undefined) {
|
|
14
|
+
throw new Error("Provide custom-field values once: use either `customFields` or `custom_fields`, not both.");
|
|
15
|
+
}
|
|
16
|
+
return camel !== undefined ? camel : snake;
|
|
17
|
+
}
|
|
3
18
|
export function registerContactTools(server, client) {
|
|
4
19
|
server.tool("conduyt_list_contacts", "List and search contacts with filtering. Returns paginated results with name, email, phone, tags, source, and assignee.", {
|
|
5
20
|
search: z.string().optional().describe("Search by name, email, or phone number"),
|
|
@@ -39,19 +54,27 @@ export function registerContactTools(server, client) {
|
|
|
39
54
|
company: z.string().optional().describe("Company name"),
|
|
40
55
|
source: z.string().optional().describe("Lead source (e.g. 'website', 'referral', 'import')"),
|
|
41
56
|
tags: z.array(z.string()).optional().describe("Tag UUIDs to apply (tags must already exist — create them with conduyt_create_tag first)"),
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
customFields: z.record(z.any()).optional().describe("Structured custom-field values keyed by field key, e.g. { lead_temperature: 'hot', budget: 50000 }. Define fields first with conduyt_create_custom_field (entity 'contact')."),
|
|
58
|
+
custom_fields: z.record(z.any()).optional().describe("Alias for customFields (snake_case). Provide only one of customFields / custom_fields."),
|
|
59
|
+
}, async ({ customFields, custom_fields, ...rest }) => {
|
|
60
|
+
const cf = resolveCustomFields(customFields, custom_fields);
|
|
61
|
+
const body = cf !== undefined ? { ...rest, customFields: cf } : rest;
|
|
62
|
+
const result = await client.post("/api/v1/contacts", body);
|
|
44
63
|
return formatResult(result);
|
|
45
64
|
});
|
|
46
|
-
server.tool("conduyt_update_contact", "Update an existing contact's fields — name, email, phone, company, custom fields.", {
|
|
65
|
+
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).", {
|
|
47
66
|
id: z.string().describe("Contact UUID"),
|
|
48
67
|
email: z.string().optional().describe("Updated email"),
|
|
49
68
|
firstName: z.string().optional().describe("Updated first name"),
|
|
50
69
|
lastName: z.string().optional().describe("Updated last name"),
|
|
51
70
|
phone: z.string().optional().describe("Updated phone"),
|
|
52
71
|
company: z.string().optional().describe("Updated company name"),
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
customFields: z.record(z.any()).optional().describe("Structured custom-field values to merge, keyed by field key. Omit a key to leave it unchanged."),
|
|
73
|
+
custom_fields: z.record(z.any()).optional().describe("Alias for customFields (snake_case). Provide only one of customFields / custom_fields."),
|
|
74
|
+
}, async ({ id, customFields, custom_fields, ...updates }) => {
|
|
75
|
+
const cf = resolveCustomFields(customFields, custom_fields);
|
|
76
|
+
const body = cf !== undefined ? { ...updates, customFields: cf } : updates;
|
|
77
|
+
const result = await client.patch(`/api/v1/contacts/${id}`, body);
|
|
55
78
|
return formatResult(result);
|
|
56
79
|
});
|
|
57
80
|
server.tool("conduyt_add_tags", "Add one or more existing tags to a contact by tag UUID. Tags must already exist (create them with conduyt_create_tag); this does NOT create tags.", {
|
|
@@ -61,4 +84,12 @@ export function registerContactTools(server, client) {
|
|
|
61
84
|
const result = await client.post(`/api/v1/contacts/${contactId}/tags`, { tagIds });
|
|
62
85
|
return formatResult(result);
|
|
63
86
|
});
|
|
87
|
+
server.tool("conduyt_link_contact_to_deal", "Associate an existing contact with an existing deal (creates a deal↔contact link). A deal's primary contact is set at create time via contactId; use this to attach ADDITIONAL contacts (e.g. a decision-maker plus an influencer) to the same deal. Both must already exist and belong to your account.", {
|
|
88
|
+
dealId: z.string().describe("Deal UUID"),
|
|
89
|
+
contactId: z.string().describe("Contact UUID to link to the deal"),
|
|
90
|
+
role: z.string().optional().describe("Relationship role for this contact on the deal, e.g. 'decision_maker', 'influencer', 'billing'. Defaults to 'related'."),
|
|
91
|
+
}, async ({ dealId, contactId, role }) => {
|
|
92
|
+
const result = await client.post(`/api/v1/deals/${dealId}/contacts`, { contactId, role });
|
|
93
|
+
return formatResult(result);
|
|
94
|
+
});
|
|
64
95
|
}
|
package/dist/tools/deals.js
CHANGED
|
@@ -1,20 +1,57 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { formatResult } from "../client.js";
|
|
3
3
|
export function registerDealTools(server, client) {
|
|
4
|
-
server.tool("conduyt_list_deals", "List deals with filtering by pipeline, stage, assignee, or status. Returns deal title, value, stage, contact, and dates.", {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
server.tool("conduyt_list_deals", "List deals with filtering by pipeline, stage, assignee, or status. Returns deal title, value, stage, contact, and dates. Filter by pipeline/stage NAME (the same names you import with) or by UUID — both camelCase (pipelineName) and snake_case (pipeline_name) argument names are accepted. An unknown name returns a clear error, never the whole account.", {
|
|
5
|
+
pipeline: z.string().optional().describe("Filter by pipeline UUID (canonical). Aliases: pipelineId, pipeline_id."),
|
|
6
|
+
pipelineId: z.string().optional().describe("Filter by pipeline UUID (alias of pipeline)."),
|
|
7
|
+
pipeline_id: z.string().optional().describe("snake_case alias of pipelineId."),
|
|
8
|
+
pipelineName: z.string().optional().describe("Filter by pipeline NAME (the same name used in conduyt_import_deals/create) — resolved to its UUID; errors if not found. Alias: pipeline_name."),
|
|
9
|
+
pipeline_name: z.string().optional().describe("snake_case alias of pipelineName."),
|
|
10
|
+
stage: z.string().optional().describe("Filter by stage UUID (canonical). Aliases: stageId, stage_id."),
|
|
11
|
+
stageId: z.string().optional().describe("Filter by stage UUID (alias of stage)."),
|
|
12
|
+
stage_id: z.string().optional().describe("snake_case alias of stageId."),
|
|
13
|
+
stageName: z.string().optional().describe("Filter by stage NAME within the pipeline — resolved to its UUID; errors if not found. Alias: stage_name."),
|
|
14
|
+
stage_name: z.string().optional().describe("snake_case alias of stageName."),
|
|
7
15
|
assignedTo: z.string().optional().describe("Filter by assigned user UUID"),
|
|
8
16
|
status: z.enum(["open", "won", "lost"]).optional().describe("Filter by deal status"),
|
|
9
17
|
search: z.string().optional().describe("Search deal titles"),
|
|
10
18
|
page: z.number().optional().describe("Page number (default 1)"),
|
|
11
19
|
per_page: z.number().optional().describe("Results per page (default 25, max 100)"),
|
|
12
|
-
}, async (
|
|
20
|
+
}, async (a) => {
|
|
13
21
|
const params = new URLSearchParams();
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
// Accept every reasonable casing of each filter (canonical bare name +
|
|
23
|
+
// camelCase + snake_case) so an agent's provided filter is NEVER silently
|
|
24
|
+
// stripped by the schema. Coalesce each alias GROUP to a single value
|
|
25
|
+
// locally — conflicting non-blank values in a group are a contradictory
|
|
26
|
+
// request and raise a clear tool error (don't emit ambiguous duplicate
|
|
27
|
+
// query params and hope the parser picks the right one) — then send ONE
|
|
28
|
+
// canonical wire param (pipeline / stage / pipeline_name / stage_name, the
|
|
29
|
+
// params the live contract advertises). A blank value is forwarded so the
|
|
30
|
+
// server rejects it; an id/name cross-conflict is caught by the server.
|
|
31
|
+
const coalesce = (label, ...vals) => {
|
|
32
|
+
const present = vals.filter((v) => v !== undefined);
|
|
33
|
+
if (present.length === 0)
|
|
34
|
+
return undefined;
|
|
35
|
+
const distinct = [...new Set(present.map((v) => v.trim().toLowerCase()).filter((v) => v !== ""))];
|
|
36
|
+
if (distinct.length > 1) {
|
|
37
|
+
throw new Error(`Conflicting ${label} values — pass only one.`);
|
|
38
|
+
}
|
|
39
|
+
// prefer a non-blank original; else keep a blank so the server can reject it
|
|
40
|
+
return present.find((v) => v.trim() !== "") ?? present[0];
|
|
41
|
+
};
|
|
42
|
+
const { assignedTo, status, search, page, per_page } = a;
|
|
43
|
+
const pipelineV = coalesce("pipeline id", a.pipeline, a.pipelineId, a.pipeline_id);
|
|
44
|
+
const pipelineNameV = coalesce("pipeline name", a.pipelineName, a.pipeline_name);
|
|
45
|
+
const stageV = coalesce("stage id", a.stage, a.stageId, a.stage_id);
|
|
46
|
+
const stageNameV = coalesce("stage name", a.stageName, a.stage_name);
|
|
47
|
+
if (pipelineV !== undefined)
|
|
48
|
+
params.set("pipeline", pipelineV);
|
|
49
|
+
if (pipelineNameV !== undefined)
|
|
50
|
+
params.set("pipeline_name", pipelineNameV);
|
|
51
|
+
if (stageV !== undefined)
|
|
52
|
+
params.set("stage", stageV);
|
|
53
|
+
if (stageNameV !== undefined)
|
|
54
|
+
params.set("stage_name", stageNameV);
|
|
18
55
|
if (assignedTo)
|
|
19
56
|
params.set("assigned_to", assignedTo);
|
|
20
57
|
if (status)
|
package/package.json
CHANGED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { ConduytClient } from "../client.js";
|
|
3
|
-
/**
|
|
4
|
-
* Document knowledge base (RAG) — tier 2 of Conduyt's AI model: "use our AI with
|
|
5
|
-
* YOUR model." Uploaded documents are retrieved and fed to the chat model on
|
|
6
|
-
* every call, so the feature is gated to accounts running their OWN AI engine
|
|
7
|
-
* (BYO key). Caps: BYO = 20 docs, + $50/mo AI Knowledge add-on = 100, >100 is
|
|
8
|
-
* handled manually ("reach out to us").
|
|
9
|
-
*
|
|
10
|
-
* All JSON surfaces (API / MCP / CLI) expose the same subset: list / add-URL /
|
|
11
|
-
* delete / reindex. Binary file upload (TXT/MD/CSV) is multipart and is web-UI
|
|
12
|
-
* only — neither MCP tool calls nor the generated CLI carry a file body.
|
|
13
|
-
*/
|
|
14
|
-
export declare function registerKnowledgeTools(server: McpServer, client: ConduytClient): void;
|
package/dist/tools/knowledge.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { formatResult } from "../client.js";
|
|
3
|
-
/**
|
|
4
|
-
* Document knowledge base (RAG) — tier 2 of Conduyt's AI model: "use our AI with
|
|
5
|
-
* YOUR model." Uploaded documents are retrieved and fed to the chat model on
|
|
6
|
-
* every call, so the feature is gated to accounts running their OWN AI engine
|
|
7
|
-
* (BYO key). Caps: BYO = 20 docs, + $50/mo AI Knowledge add-on = 100, >100 is
|
|
8
|
-
* handled manually ("reach out to us").
|
|
9
|
-
*
|
|
10
|
-
* All JSON surfaces (API / MCP / CLI) expose the same subset: list / add-URL /
|
|
11
|
-
* delete / reindex. Binary file upload (TXT/MD/CSV) is multipart and is web-UI
|
|
12
|
-
* only — neither MCP tool calls nor the generated CLI carry a file body.
|
|
13
|
-
*/
|
|
14
|
-
export function registerKnowledgeTools(server, client) {
|
|
15
|
-
server.tool("conduyt_list_knowledge_sources", "List the account's AI knowledge-base documents and the current entitlement. Returns each source (name, type, status: queued|indexing|indexed|stale|failed) plus byoActive (whether the document KB is unlocked by an own-AI-key), addon (whether the $50/mo add-on is active), docLimit, and used count.", {}, async () => {
|
|
16
|
-
const result = await client.get("/api/v1/knowledge/sources");
|
|
17
|
-
return formatResult(result);
|
|
18
|
-
});
|
|
19
|
-
server.tool("conduyt_add_knowledge_url", "Add a web page to the AI knowledge base by URL. The page is scraped, chunked, embedded, and becomes retrievable for AI answers. Requires the account to have its own AI key connected (byoActive) and to be under its document cap. To add a file (TXT/MD/CSV), use the web UI — file upload is multipart and not available over MCP or CLI.", {
|
|
20
|
-
url: z.string().describe("Public http(s) URL of the page to learn from"),
|
|
21
|
-
name: z.string().optional().describe("Optional friendly name (defaults to the URL)"),
|
|
22
|
-
}, async (params) => {
|
|
23
|
-
const result = await client.post("/api/v1/knowledge/sources", params);
|
|
24
|
-
return formatResult(result);
|
|
25
|
-
});
|
|
26
|
-
server.tool("conduyt_delete_knowledge_source", "Remove a document from the AI knowledge base. Deletes the stored file (if any) and all of its embeddings. Account-scoped.", {
|
|
27
|
-
id: z.string().describe("Knowledge source UUID"),
|
|
28
|
-
}, async ({ id }) => {
|
|
29
|
-
const result = await client.del(`/api/v1/knowledge/sources/${id}`);
|
|
30
|
-
return formatResult(result);
|
|
31
|
-
});
|
|
32
|
-
server.tool("conduyt_reindex_knowledge_source", "Re-learn a document — re-fetches/re-extracts and rebuilds its embeddings. Use to refresh a URL whose content changed, or to retry a source that failed indexing.", {
|
|
33
|
-
id: z.string().describe("Knowledge source UUID"),
|
|
34
|
-
}, async ({ id }) => {
|
|
35
|
-
const result = await client.post(`/api/v1/knowledge/sources/${id}/reindex`, {});
|
|
36
|
-
return formatResult(result);
|
|
37
|
-
});
|
|
38
|
-
}
|