conduyt-mcp 4.6.0 → 4.9.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/client.js +14 -1
- package/dist/index.js +11 -1
- package/dist/tools/bulk.js +1 -1
- package/dist/tools/calls.d.ts +9 -0
- package/dist/tools/calls.js +49 -0
- package/dist/tools/dnc.d.ts +13 -0
- package/dist/tools/dnc.js +50 -0
- package/dist/tools/messaging.js +44 -0
- package/dist/tools/saved-filters.d.ts +3 -0
- package/dist/tools/saved-filters.js +75 -0
- package/dist/tools/tasks.js +32 -0
- package/package.json +4 -3
package/dist/client.js
CHANGED
|
@@ -2,7 +2,20 @@ export class ConduytClient {
|
|
|
2
2
|
baseUrl;
|
|
3
3
|
apiKey;
|
|
4
4
|
constructor(config) {
|
|
5
|
-
|
|
5
|
+
// Strip trailing slashes, then guard against a misconfigured base URL that
|
|
6
|
+
// already includes the API prefix. Every tool builds paths as `/api/v1/...`,
|
|
7
|
+
// so a base of `https://conduyt.app/api/v1` would produce
|
|
8
|
+
// `/api/v1/api/v1/...` and 404 every call. Strip a trailing `/api/v1`
|
|
9
|
+
// (optionally with a version suffix) and warn so it surfaces in logs.
|
|
10
|
+
let base = config.apiUrl.replace(/\/+$/, "");
|
|
11
|
+
const dupPrefix = /\/api\/v\d+$/i;
|
|
12
|
+
if (dupPrefix.test(base)) {
|
|
13
|
+
const stripped = base.replace(dupPrefix, "");
|
|
14
|
+
console.error(`CONDUYT_API_URL should be the bare origin (e.g. https://conduyt.app), not include the API prefix. ` +
|
|
15
|
+
`Stripping "${base.slice(stripped.length)}" so requests don't 404 as /api/v1/api/v1/...`);
|
|
16
|
+
base = stripped;
|
|
17
|
+
}
|
|
18
|
+
this.baseUrl = base;
|
|
6
19
|
this.apiKey = config.apiKey;
|
|
7
20
|
}
|
|
8
21
|
async request(method, path, body) {
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
5
|
import { ConduytClient } from "./client.js";
|
|
5
6
|
import { registerDiscoveryTools } from "./tools/discovery.js";
|
|
@@ -12,6 +13,7 @@ import { registerAiTools } from "./tools/ai.js";
|
|
|
12
13
|
import { registerAutomationTools } from "./tools/automations.js";
|
|
13
14
|
import { registerSequenceTools } from "./tools/sequences.js";
|
|
14
15
|
import { registerMessagingTools } from "./tools/messaging.js";
|
|
16
|
+
import { registerCallTools } from "./tools/calls.js";
|
|
15
17
|
import { registerCompanyTools } from "./tools/companies.js";
|
|
16
18
|
import { registerTagTools } from "./tools/tags.js";
|
|
17
19
|
import { registerCalendarTools } from "./tools/calendar.js";
|
|
@@ -23,6 +25,8 @@ import { registerCustomFieldTools } from "./tools/custom-fields.js";
|
|
|
23
25
|
import { registerSmartListTools } from "./tools/smart-lists.js";
|
|
24
26
|
import { registerUserTools } from "./tools/users.js";
|
|
25
27
|
import { registerBulkTools } from "./tools/bulk.js";
|
|
28
|
+
import { registerDncTools } from "./tools/dnc.js";
|
|
29
|
+
import { registerSavedFilterTools } from "./tools/saved-filters.js";
|
|
26
30
|
import { registerDripCampaignTools } from "./tools/drip-campaigns.js";
|
|
27
31
|
import { registerScoringTools } from "./tools/scoring.js";
|
|
28
32
|
import { registerProductTools } from "./tools/products.js";
|
|
@@ -43,9 +47,12 @@ if (!apiKey.startsWith("cdy_")) {
|
|
|
43
47
|
process.exit(1);
|
|
44
48
|
}
|
|
45
49
|
const client = new ConduytClient({ apiUrl, apiKey });
|
|
50
|
+
// Single source of truth for the version — read from package.json so the
|
|
51
|
+
// runtime initialize metadata can never drift from the published package.
|
|
52
|
+
const pkg = createRequire(import.meta.url)("../package.json");
|
|
46
53
|
const server = new McpServer({
|
|
47
54
|
name: "conduyt",
|
|
48
|
-
version:
|
|
55
|
+
version: pkg.version,
|
|
49
56
|
});
|
|
50
57
|
registerDiscoveryTools(server, client);
|
|
51
58
|
registerContactTools(server, client);
|
|
@@ -57,6 +64,7 @@ registerAiTools(server, client);
|
|
|
57
64
|
registerAutomationTools(server, client);
|
|
58
65
|
registerSequenceTools(server, client);
|
|
59
66
|
registerMessagingTools(server, client);
|
|
67
|
+
registerCallTools(server, client);
|
|
60
68
|
registerCompanyTools(server, client);
|
|
61
69
|
registerTagTools(server, client);
|
|
62
70
|
registerCalendarTools(server, client);
|
|
@@ -68,6 +76,8 @@ registerCustomFieldTools(server, client);
|
|
|
68
76
|
registerSmartListTools(server, client);
|
|
69
77
|
registerUserTools(server, client);
|
|
70
78
|
registerBulkTools(server, client);
|
|
79
|
+
registerDncTools(server, client);
|
|
80
|
+
registerSavedFilterTools(server, client);
|
|
71
81
|
registerDripCampaignTools(server, client);
|
|
72
82
|
registerScoringTools(server, client);
|
|
73
83
|
registerProductTools(server, client);
|
package/dist/tools/bulk.js
CHANGED
|
@@ -103,7 +103,7 @@ export function registerBulkTools(server, client) {
|
|
|
103
103
|
const result = await client.post("/api/v1/bulk/contacts/delete", params);
|
|
104
104
|
return formatResult(result);
|
|
105
105
|
});
|
|
106
|
-
server.tool("conduyt_bulk_dnc_contacts", "Add or remove a Do-Not-Contact / litigator mark for many contacts (compliance action).
|
|
106
|
+
server.tool("conduyt_bulk_dnc_contacts", "Add or remove a Do-Not-Contact / litigator mark for many contacts (compliance action). Requires the EXPLICIT 'dnc' API-key scope — an admin must grant it deliberately in Settings > API Keys (an unrestricted key does NOT imply it; without the scope this returns 403). Writes are also bound by contact-ownership rules for limited keys. To CHECK whether a number is already blocked, use conduyt_get_contact (dnc status) or the contacts dnc-status read.", {
|
|
107
107
|
contactIds: z.array(z.string()).describe("Contact UUIDs (those without a phone number are skipped)"),
|
|
108
108
|
action: z.enum(["add", "remove"]).describe("Add or remove the mark"),
|
|
109
109
|
tier: z.enum(["dnc", "litigator"]).describe("Which list: 'dnc' (standard do-not-contact) or 'litigator' (known litigator; admin-only to add)"),
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { ConduytClient } from "../client.js";
|
|
3
|
+
/**
|
|
4
|
+
* READ-ONLY dialer/call-history tools. Live call control (initiate, mute,
|
|
5
|
+
* hold, transfer, hangup, vm-drop, record) is deliberately NOT exposed here —
|
|
6
|
+
* those actions are human-session-only by design. These tools let an agent
|
|
7
|
+
* review the call log and inspect a single call's outcome/transcript.
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerCallTools(server: McpServer, client: ConduytClient): void;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatResult } from "../client.js";
|
|
3
|
+
/**
|
|
4
|
+
* READ-ONLY dialer/call-history tools. Live call control (initiate, mute,
|
|
5
|
+
* hold, transfer, hangup, vm-drop, record) is deliberately NOT exposed here —
|
|
6
|
+
* those actions are human-session-only by design. These tools let an agent
|
|
7
|
+
* review the call log and inspect a single call's outcome/transcript.
|
|
8
|
+
*/
|
|
9
|
+
export function registerCallTools(server, client) {
|
|
10
|
+
server.tool("conduyt_list_calls", "List call-history records (read-only) with filters — review past inbound/outbound calls, their status, duration, and disposition. Newest first. Returns metadata only by default (NO call transcripts, NO recording/voicemail URLs — only hasRecording/hasVoicemail/hasTranscript flags), so a routine list never pulls raw transcripts into context. To read a specific call's transcript, use conduyt_get_call. Requires dialer:view.", {
|
|
11
|
+
contactId: z.string().optional().describe("Filter to calls for a single contact UUID"),
|
|
12
|
+
userId: z.string().optional().describe("Filter to calls handled by a specific user UUID"),
|
|
13
|
+
direction: z.enum(["inbound", "outbound"]).optional().describe("Filter by call direction"),
|
|
14
|
+
status: z.string().optional().describe("Filter by call status, e.g. 'completed', 'initiated', 'no-answer', 'failed'"),
|
|
15
|
+
dateFrom: z.string().optional().describe("Only calls created on/after this date (ISO 8601)"),
|
|
16
|
+
dateTo: z.string().optional().describe("Only calls created on/before this date (ISO 8601)"),
|
|
17
|
+
page: z.number().optional().describe("Page number (default 1)"),
|
|
18
|
+
per_page: z.number().optional().describe("Results per page (default 50, max 200)"),
|
|
19
|
+
}, async ({ contactId, userId, direction, status, dateFrom, dateTo, page, per_page }) => {
|
|
20
|
+
const params = new URLSearchParams();
|
|
21
|
+
// Metadata-only by design: the minimal route never loads transcriptionText
|
|
22
|
+
// (heavy @db.Text) — transcript retrieval is a deliberate conduyt_get_call.
|
|
23
|
+
params.set("minimal", "1");
|
|
24
|
+
if (contactId)
|
|
25
|
+
params.set("contact_id", contactId);
|
|
26
|
+
if (userId)
|
|
27
|
+
params.set("user_id", userId);
|
|
28
|
+
if (direction)
|
|
29
|
+
params.set("direction", direction);
|
|
30
|
+
if (status)
|
|
31
|
+
params.set("status", status);
|
|
32
|
+
if (dateFrom)
|
|
33
|
+
params.set("date_from", dateFrom);
|
|
34
|
+
if (dateTo)
|
|
35
|
+
params.set("date_to", dateTo);
|
|
36
|
+
if (page)
|
|
37
|
+
params.set("page", String(page));
|
|
38
|
+
if (per_page)
|
|
39
|
+
params.set("per_page", String(per_page));
|
|
40
|
+
const result = await client.get(`/api/v1/calls?${params}`);
|
|
41
|
+
return formatResult(result);
|
|
42
|
+
});
|
|
43
|
+
server.tool("conduyt_get_call", "Get a single call record by UUID (read-only) — direction, status, duration, disposition, notes, transcript text (if any), and the linked contact/user. Recording/voicemail URLs are withheld; hasRecording/hasVoicemail flags indicate their presence. Requires dialer:view.", {
|
|
44
|
+
id: z.string().describe("Call UUID"),
|
|
45
|
+
}, async ({ id }) => {
|
|
46
|
+
const result = await client.get(`/api/v1/calls/${encodeURIComponent(id)}`);
|
|
47
|
+
return formatResult(result);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { ConduytClient } from "../client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Do-Not-Call / litigator list management (compliance surface).
|
|
5
|
+
*
|
|
6
|
+
* All of these require the EXPLICIT 'dnc' API-key scope — an admin must grant
|
|
7
|
+
* it deliberately in Settings > API Keys; an unrestricted key does NOT imply
|
|
8
|
+
* it (403 without it). Limited keys are additionally bound by contact
|
|
9
|
+
* ownership rules. Entries are phone-scoped when a number is given
|
|
10
|
+
* (normalizedPhone blocks the NUMBER account-wide) and contact-scoped when
|
|
11
|
+
* only a contactId is given.
|
|
12
|
+
*/
|
|
13
|
+
export declare function registerDncTools(server: McpServer, client: ConduytClient): void;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatResult } from "../client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Do-Not-Call / litigator list management (compliance surface).
|
|
5
|
+
*
|
|
6
|
+
* All of these require the EXPLICIT 'dnc' API-key scope — an admin must grant
|
|
7
|
+
* it deliberately in Settings > API Keys; an unrestricted key does NOT imply
|
|
8
|
+
* it (403 without it). Limited keys are additionally bound by contact
|
|
9
|
+
* ownership rules. Entries are phone-scoped when a number is given
|
|
10
|
+
* (normalizedPhone blocks the NUMBER account-wide) and contact-scoped when
|
|
11
|
+
* only a contactId is given.
|
|
12
|
+
*/
|
|
13
|
+
export function registerDncTools(server, client) {
|
|
14
|
+
server.tool("conduyt_list_dnc", "List active Do-Not-Call / litigator entries. Filter by search (phone or contact name) or tier. Requires the explicit 'dnc' API-key scope.", {
|
|
15
|
+
search: z.string().optional().describe("Match on phone number or contact name"),
|
|
16
|
+
tier: z.enum(["dnc", "litigator"]).optional().describe("Filter to one list"),
|
|
17
|
+
page: z.number().optional().describe("Page number (default 1)"),
|
|
18
|
+
per_page: z.number().optional().describe("Results per page (default 50)"),
|
|
19
|
+
}, async ({ search, tier, page, per_page }) => {
|
|
20
|
+
const params = new URLSearchParams();
|
|
21
|
+
if (search)
|
|
22
|
+
params.set("search", search);
|
|
23
|
+
if (tier)
|
|
24
|
+
params.set("tier", tier);
|
|
25
|
+
if (page)
|
|
26
|
+
params.set("page", String(page));
|
|
27
|
+
if (per_page)
|
|
28
|
+
params.set("per_page", String(per_page));
|
|
29
|
+
const result = await client.get(`/api/v1/dnc?${params}`);
|
|
30
|
+
return formatResult(result);
|
|
31
|
+
});
|
|
32
|
+
server.tool("conduyt_add_dnc", "Add a phone number (or a contact) to the Do-Not-Call or litigator list — e.g. \"add this number to the do not call list\". Phone entries block the NUMBER account-wide across dialer/SMS. Requires the explicit 'dnc' API-key scope; adding to the litigator tier is admin-only.", {
|
|
33
|
+
phone: z.string().optional().describe("Phone number to block (E.164 preferred, e.g. +15551234567). Provide phone and/or contactId."),
|
|
34
|
+
contactId: z.string().optional().describe("Contact UUID to mark (contact-scoped block when no phone is supplied)"),
|
|
35
|
+
tier: z.enum(["dnc", "litigator"]).default("dnc").describe("Which list: 'dnc' (standard) or 'litigator' (known litigator; admin-only)"),
|
|
36
|
+
reason: z.string().optional().describe("Reason recorded for the compliance audit trail"),
|
|
37
|
+
}, async (params) => {
|
|
38
|
+
const result = await client.post("/api/v1/dnc", params);
|
|
39
|
+
return formatResult(result);
|
|
40
|
+
});
|
|
41
|
+
server.tool("conduyt_remove_dnc", "Remove (soft-delete) a Do-Not-Call / litigator entry by its entry id — unblocks the number/contact. Use conduyt_list_dnc to find the entry id first. Requires the explicit 'dnc' API-key scope.", {
|
|
42
|
+
// Strict UUID + encode: a raw string interpolated into a DELETE path
|
|
43
|
+
// could traverse (../contacts/<id>) into a different destructive
|
|
44
|
+
// endpoint — this tool must stay DNC-only regardless of key scope.
|
|
45
|
+
id: z.string().uuid().describe("DNC entry UUID (from conduyt_list_dnc)"),
|
|
46
|
+
}, async ({ id }) => {
|
|
47
|
+
const result = await client.del(`/api/v1/dnc/${encodeURIComponent(id)}`);
|
|
48
|
+
return formatResult(result);
|
|
49
|
+
});
|
|
50
|
+
}
|
package/dist/tools/messaging.js
CHANGED
|
@@ -22,4 +22,48 @@ export function registerMessagingTools(server, client) {
|
|
|
22
22
|
const result = await client.post("/api/v1/email/send", params);
|
|
23
23
|
return formatResult(result);
|
|
24
24
|
});
|
|
25
|
+
server.tool("conduyt_get_conversation", "Read the full conversation thread for one contact — the chronological SMS/email message history (both inbound replies and outbound sends), plus conversation state, unread count, and recent notes. Use this to see what a contact has replied before deciding how to follow up. Requires communications:view.", {
|
|
26
|
+
contactId: z.string().describe("Contact UUID whose conversation to read"),
|
|
27
|
+
channel: z.string().optional().describe("Filter to one channel, e.g. 'sms' or 'email' (omit for all channels)"),
|
|
28
|
+
cursor: z.string().optional().describe("Message ID for cursor pagination — returns messages OLDER than this id (for loading earlier history)"),
|
|
29
|
+
around: z.string().optional().describe("Message ID to center the window on — returns messages before and after this id"),
|
|
30
|
+
per_page: z.number().optional().describe("Messages per page (default 50, max 100)"),
|
|
31
|
+
}, async ({ contactId, channel, cursor, around, per_page }) => {
|
|
32
|
+
const params = new URLSearchParams();
|
|
33
|
+
if (channel)
|
|
34
|
+
params.set("channel", channel);
|
|
35
|
+
if (cursor)
|
|
36
|
+
params.set("cursor", cursor);
|
|
37
|
+
if (around)
|
|
38
|
+
params.set("around", around);
|
|
39
|
+
if (per_page)
|
|
40
|
+
params.set("per_page", String(per_page));
|
|
41
|
+
const qs = params.toString();
|
|
42
|
+
const result = await client.get(`/api/v1/conversations/${encodeURIComponent(contactId)}${qs ? `?${qs}` : ""}`);
|
|
43
|
+
return formatResult(result);
|
|
44
|
+
});
|
|
45
|
+
server.tool("conduyt_list_messages", "List individual messages across contacts with filters — read inbound replies and outbound sends account-wide, or scoped to one contact. Newest first. Use direction='inbound' to surface replies that need a response. Requires communications:view.", {
|
|
46
|
+
contactId: z.string().optional().describe("Filter to a single contact UUID"),
|
|
47
|
+
channel: z.string().optional().describe("Filter by channel, e.g. 'sms' or 'email'"),
|
|
48
|
+
direction: z.enum(["inbound", "outbound"]).optional().describe("Filter by direction — 'inbound' = messages from the contact, 'outbound' = messages you sent"),
|
|
49
|
+
status: z.string().optional().describe("Filter by message status, e.g. 'sent', 'delivered', 'received', 'failed', 'draft'"),
|
|
50
|
+
page: z.number().optional().describe("Page number (default 1)"),
|
|
51
|
+
per_page: z.number().optional().describe("Results per page (default 50, max 200)"),
|
|
52
|
+
}, async ({ contactId, channel, direction, status, page, per_page }) => {
|
|
53
|
+
const params = new URLSearchParams();
|
|
54
|
+
if (contactId)
|
|
55
|
+
params.set("contactId", contactId);
|
|
56
|
+
if (channel)
|
|
57
|
+
params.set("channel", channel);
|
|
58
|
+
if (direction)
|
|
59
|
+
params.set("direction", direction);
|
|
60
|
+
if (status)
|
|
61
|
+
params.set("status", status);
|
|
62
|
+
if (page)
|
|
63
|
+
params.set("page", String(page));
|
|
64
|
+
if (per_page)
|
|
65
|
+
params.set("per_page", String(per_page));
|
|
66
|
+
const result = await client.get(`/api/v1/messages?${params}`);
|
|
67
|
+
return formatResult(result);
|
|
68
|
+
});
|
|
25
69
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatResult } from "../client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Personal saved filters for list pages (Velocify-style). Strictly
|
|
5
|
+
* per-user: a saved filter belongs to the API key's user and is invisible
|
|
6
|
+
* to everyone else — these are NOT Smart Views (admin-curated team/dialer
|
|
7
|
+
* buckets). A saved filter snapshots an advanced-filter payload + sort so
|
|
8
|
+
* a view can be re-applied in one step (the web app's "Saved" dropdown on
|
|
9
|
+
* All Contacts). Requires the 'saved-filters' API-key scope.
|
|
10
|
+
*/
|
|
11
|
+
// Filter payload — same shapes the API accepts: the canonical GROUPED
|
|
12
|
+
// payload, or a legacy FLAT rule array which the API normalizes into a
|
|
13
|
+
// single AND group on save. Caps mirror the API's validateAdvancedFilters
|
|
14
|
+
// limits (10 groups × 20 rules) so oversized payloads fail here instead of
|
|
15
|
+
// round-tripping to a 400.
|
|
16
|
+
const ruleSchema = z.object({
|
|
17
|
+
field: z.string().describe("Filter field, e.g. assignedTo, source, tags, pipelineStage, dealStatus, createdAt"),
|
|
18
|
+
operator: z.string().describe("Operator valid for the field, e.g. equals, is_any_of, contains, is_empty"),
|
|
19
|
+
value: z.union([z.string(), z.array(z.string())]).optional(),
|
|
20
|
+
});
|
|
21
|
+
const groupSchema = z.object({
|
|
22
|
+
conjunction: z.enum(["AND", "OR"]),
|
|
23
|
+
rules: z.array(ruleSchema).max(20),
|
|
24
|
+
});
|
|
25
|
+
const filtersSchema = z
|
|
26
|
+
.union([z.array(groupSchema).max(10), z.array(ruleSchema).max(20)])
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Grouped filter payload (max 10 groups × 20 rules), e.g. [{"conjunction":"AND","rules":[{"field":"source","operator":"equals","value":"facebook"}]}]. A flat rule array is also accepted and saved as a single AND group. Omit for an empty filter set.');
|
|
29
|
+
export function registerSavedFilterTools(server, client) {
|
|
30
|
+
server.tool("conduyt_list_saved_filters", "List YOUR saved contact filters (the web app's 'Saved' dropdown on All Contacts). Each entry has a name, the grouped filter payload, and sort. Saved filters are personal to the API key's user. Requires the 'saved-filters' scope.", {
|
|
31
|
+
entity_type: z.enum(["contact"]).optional().describe("Entity type (default 'contact')"),
|
|
32
|
+
}, async ({ entity_type }) => {
|
|
33
|
+
const params = new URLSearchParams();
|
|
34
|
+
if (entity_type)
|
|
35
|
+
params.set("entity_type", entity_type);
|
|
36
|
+
const qs = params.toString();
|
|
37
|
+
const result = await client.get(`/api/v1/saved-filters${qs ? `?${qs}` : ""}`);
|
|
38
|
+
return formatResult(result);
|
|
39
|
+
});
|
|
40
|
+
server.tool("conduyt_create_saved_filter", "Save a named contact filter for one-click re-use — e.g. \"save a filter for Heather's Facebook leads\". Names are unique per user (409 on duplicate); max 50 saved filters per user. Requires the 'saved-filters' scope.", {
|
|
41
|
+
name: z.string().min(1).max(60).describe("Display name shown in the Saved dropdown (unique per user, max 60 chars)"),
|
|
42
|
+
filters: filtersSchema,
|
|
43
|
+
sortField: z.string().optional().describe("Sort field applied with the filter (default createdAt)"),
|
|
44
|
+
sortOrder: z.enum(["asc", "desc"]).optional().describe("Sort direction (default desc)"),
|
|
45
|
+
entityType: z.enum(["contact"]).optional().describe("Entity type (default 'contact')"),
|
|
46
|
+
}, async (params) => {
|
|
47
|
+
const result = await client.post("/api/v1/saved-filters", params);
|
|
48
|
+
return formatResult(result);
|
|
49
|
+
});
|
|
50
|
+
server.tool("conduyt_update_saved_filter", "Rename a saved filter or replace its filters/sort. Owner-only — you can only update your own saved filters. Use conduyt_list_saved_filters to find the id. Requires the 'saved-filters' scope.", {
|
|
51
|
+
id: z.string().uuid().describe("Saved filter UUID (from conduyt_list_saved_filters)"),
|
|
52
|
+
name: z.string().min(1).max(60).optional().describe("New display name (unique per user)"),
|
|
53
|
+
filters: filtersSchema,
|
|
54
|
+
sortField: z.string().optional(),
|
|
55
|
+
sortOrder: z.enum(["asc", "desc"]).optional(),
|
|
56
|
+
}, async ({ id, name, filters, sortField, sortOrder }) => {
|
|
57
|
+
// Inline body (undefined fields drop out in JSON) — keeps the shape
|
|
58
|
+
// readable by the contract verifier's static parser.
|
|
59
|
+
const result = await client.patch(`/api/v1/saved-filters/${encodeURIComponent(id)}`, {
|
|
60
|
+
name,
|
|
61
|
+
filters,
|
|
62
|
+
sortField,
|
|
63
|
+
sortOrder,
|
|
64
|
+
});
|
|
65
|
+
return formatResult(result);
|
|
66
|
+
});
|
|
67
|
+
server.tool("conduyt_delete_saved_filter", "Delete one of YOUR saved filters by id. Owner-only. Requires the 'saved-filters' scope.", {
|
|
68
|
+
// Strict UUID + encode — the id is interpolated into a DELETE path and
|
|
69
|
+
// must never traverse out of the saved-filters endpoint.
|
|
70
|
+
id: z.string().uuid().describe("Saved filter UUID (from conduyt_list_saved_filters)"),
|
|
71
|
+
}, async ({ id }) => {
|
|
72
|
+
const result = await client.del(`/api/v1/saved-filters/${encodeURIComponent(id)}`);
|
|
73
|
+
return formatResult(result);
|
|
74
|
+
});
|
|
75
|
+
}
|
package/dist/tools/tasks.js
CHANGED
|
@@ -62,4 +62,36 @@ export function registerTaskTools(server, client) {
|
|
|
62
62
|
const result = await client.post("/api/v1/tasks", params);
|
|
63
63
|
return formatResult(result);
|
|
64
64
|
});
|
|
65
|
+
server.tool("conduyt_get_task", "Get a single task by UUID with full details — title, description, due date, status, priority, and the linked assignee, creator, contact, and deal.", {
|
|
66
|
+
id: z.string().uuid().describe("Task UUID"),
|
|
67
|
+
}, async ({ id }) => {
|
|
68
|
+
const result = await client.get(`/api/v1/tasks/${encodeURIComponent(id)}`);
|
|
69
|
+
return formatResult(result);
|
|
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.", {
|
|
72
|
+
id: z.string().uuid().describe("Task UUID to update"),
|
|
73
|
+
title: z.string().optional().describe("Updated task title"),
|
|
74
|
+
description: z.string().optional().describe("Updated description (pass an empty string to clear it)"),
|
|
75
|
+
dueDate: z.string().optional().describe("Updated due date (ISO 8601, e.g. '2026-07-15' or a full timestamp)"),
|
|
76
|
+
priority: z.enum(["low", "medium", "high", "urgent"]).optional().describe("Updated priority"),
|
|
77
|
+
status: z.enum(["todo", "in_progress", "done"]).optional().describe("Updated status. Setting 'done' auto-stamps completedAt and fires task.completed side effects."),
|
|
78
|
+
assignedTo: z.string().nullable().optional().describe("Reassign to a user UUID. Magic strings supported: '@me' (the caller), '@owner' (the account owner). Pass null to unassign."),
|
|
79
|
+
contactId: z.string().nullable().optional().describe("Link to a contact UUID (null to unlink)"),
|
|
80
|
+
dealId: z.string().nullable().optional().describe("Link to a deal UUID (null to unlink)"),
|
|
81
|
+
}, async ({ id, ...updates }) => {
|
|
82
|
+
const result = await client.patch(`/api/v1/tasks/${id}`, updates);
|
|
83
|
+
return formatResult(result);
|
|
84
|
+
});
|
|
85
|
+
server.tool("conduyt_complete_task", "Mark a task done (the idiomatic 'complete' action). Sets status to 'done', which the server stamps with completedAt and dispatches task.completed automations/webhooks. Requires tasks:edit.", {
|
|
86
|
+
id: z.string().uuid().describe("Task UUID to mark done"),
|
|
87
|
+
}, async ({ id }) => {
|
|
88
|
+
const result = await client.patch(`/api/v1/tasks/${encodeURIComponent(id)}`, { status: "done" });
|
|
89
|
+
return formatResult(result);
|
|
90
|
+
});
|
|
91
|
+
server.tool("conduyt_delete_task", "Delete a task by UUID (hard delete — the task row is removed and a task_deleted activity is logged for audit). There is no undelete endpoint, so treat it as irreversible via the API. Requires tasks:delete.", {
|
|
92
|
+
id: z.string().uuid().describe("Task UUID to delete"),
|
|
93
|
+
}, async ({ id }) => {
|
|
94
|
+
const result = await client.del(`/api/v1/tasks/${encodeURIComponent(id)}`);
|
|
95
|
+
return formatResult(result);
|
|
96
|
+
});
|
|
65
97
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "conduyt-mcp",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.9.0",
|
|
4
4
|
"description": "MCP server for Conduyt CRM — expose CRM operations as AI-accessible tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
"start": "tsx src/index.ts",
|
|
21
21
|
"build": "tsc",
|
|
22
22
|
"typecheck": "tsc --noEmit",
|
|
23
|
-
"prepublishOnly": "npm run build && ENFORCE=1 node scripts/verify-contracts.mjs",
|
|
23
|
+
"prepublishOnly": "npm run check:version && npm run build && ENFORCE=1 node scripts/verify-contracts.mjs",
|
|
24
24
|
"audit:contracts": "node scripts/verify-contracts.mjs",
|
|
25
|
-
"contracts:refresh": "node scripts/refresh-contracts.mjs"
|
|
25
|
+
"contracts:refresh": "node scripts/refresh-contracts.mjs",
|
|
26
|
+
"check:version": "node scripts/check-version-sync.mjs"
|
|
26
27
|
},
|
|
27
28
|
"keywords": [
|
|
28
29
|
"mcp",
|