conduyt-mcp 4.101.0 → 4.102.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 -2
- package/dist/client.d.ts +1 -1
- package/dist/client.js +1 -1
- package/dist/register-all.js +2 -0
- package/dist/tool-annotations.js +6 -0
- package/dist/tools/direct-uploads.d.ts +41 -0
- package/dist/tools/direct-uploads.js +113 -0
- package/package.json +1 -1
- package/scripts/coverage-manifest.json +1 -1
- package/scripts/coverage-matrix.json +47 -11
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
MCP server for [Conduyt CRM](https://conduyt.app) — expose your CRM as AI-accessible tools. Covers the core CRM surface (contacts, deals, pipelines, custom fields, automations, messaging, and more); use `conduyt_api_catalog` to discover the full set of 600+ REST endpoints.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 372 Tools
|
|
6
6
|
|
|
7
7
|
### Discovery
|
|
8
8
|
| Tool | Description |
|
|
@@ -37,6 +37,7 @@ MCP server for [Conduyt CRM](https://conduyt.app) — expose your CRM as AI-acce
|
|
|
37
37
|
| `conduyt_get_calling_hours` / `conduyt_update_calling_hours` | Read the window the dialer enforces (`effectiveCallingWindow`: `source`, `enabled`, `start`/`end` HH:MM, `timezone` or null = contact zone, then number area code, then account) / set `settings.callingWindow` (`enabled`, `start`, `end` always sent, `end` after `start`, `timezone` trimmed and recognised by the API (omit keeps the stored zone, null clears it); `enabled: false` needs `acknowledgeAnyHour: true`, else 400 `calling_hours_acknowledgement_required`) |
|
|
38
38
|
| `conduyt_list_email_opt_outs` / `conduyt_add_email_opt_out` / `conduyt_resubscribe_email_opt_out` | The email opt-out list (owner / admin key; hash-only rows `{ id, source, reason, createdAt }`): list (`page`, `perPage` 1-100), add an address by hand (`reason` 1-200 required; 201 new row, 200 when an unsubscribe-link or Mailchimp row is promoted to manual, 409 with the existing row when already listed by hand) and take an address off (`{ removed }`) |
|
|
39
39
|
| `conduyt_get_my_profile` | Read the caller's own identity (`GET /users/me`, same as `/auth/me`): `data.user` incl. `ringtone` (the incoming-ring slug, null = Conduyt Default), `data.account`, role, permissions, `apiKey`, `accounts`. Editing the profile is a signed-in person's action and is not exposed to API keys |
|
|
40
|
+
| `conduyt_create_file_upload_intent` / `conduyt_finalize_file_upload` / `conduyt_create_chat_upload_intent` / `conduyt_finalize_chat_upload` / `conduyt_upload_local_file` / `conduyt_upload_chat_attachment` | Direct-to-storage uploads (#565): intent (`POST /files/upload/intent`, `POST /chat/channels/{id}/upload/intent`; `fileName`, `size`, optional `contentType`) returns a signed PUT URL, the bytes go straight to storage, finalize (`.../finalize`, `{ uploadId }`) settles it. CRM files up to 50 MB (key scope `files`), Team Chat attachments up to 10 MB (key scope `chat`). `conduyt_upload_local_file` / `conduyt_upload_chat_attachment` do all three steps for a local path and refuse an over-cap or empty file before any call (413 wording `File is too large (12 MB). Maximum allowed: 10 MB.`). Finalize refusals, verbatim: 400 `upload_id_required`, 404 `upload_not_found`, 409 `upload_incomplete` (bytes never PUT), 409 `upload_in_progress`, 413 `file_too_large`, 422 `upload_size_mismatch`, 400/415 `file_rejected`, 429, 503 `uploads_unavailable`; finalize allows 120 s. Only a finalize 409 `upload_in_progress` is repeated (at most 4 total attempts) |
|
|
40
41
|
| `conduyt_create_short_link` | Make (or reuse) the workspace's Conduyt short link for a public link-shortener address the Message check flagged (`POST /short-links`, body `{ url }`; key needs the `messages` scope, member needs communications.create). Not shortenable: our own host, merge fields, `mailto:`/`tel:`/`sms:`, or any address the Message check does not flag as a link shortener (400 `not_shortenable`). Other errors verbatim, never retried: 400 `invalid_body`, 403 `forbidden`, 413 `body_too_large`, 429 `rate_limited` (with Retry-After), 503 `short_link_unavailable` (nothing changed) |
|
|
41
42
|
| `conduyt_get_deal_card_fields` / `conduyt_set_deal_card_fields` | Read / replace the kanban card fields (1-8, built-ins or `custom:<fieldKey>`) |
|
|
42
43
|
| `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` |
|
|
@@ -397,7 +398,7 @@ A refused call (HTTP 4xx/5xx) fails the tool with this text, stable across tools
|
|
|
397
398
|
Conduyt API <status>: <server message>[ [Retry-After: <N>s]][ <details>]
|
|
398
399
|
```
|
|
399
400
|
|
|
400
|
-
`[Retry-After: Ns]` appears only when the server sent the header (shown, never acted on: a mutation is never retried).
|
|
401
|
+
`[Retry-After: Ns]` appears only when the server sent the header (shown, never acted on: a mutation is never retried; the one exception is the finalize POST of the upload convenience tools `conduyt_upload_local_file` and `conduyt_upload_chat_attachment`: on a 409 `upload_in_progress` only, at most 4 total attempts, the Retry-After is honoured (capped at 10 s, 5 s when absent or unreadable); finalize is idempotent on the server for the same upload id, so a repeat cannot double-finalize).
|
|
401
402
|
`<details>` appears only when the body had more than `error`: it is ONE JSON object and the FINAL token. To extract it, try
|
|
402
403
|
each `{` from the left until the rest of the text parses as JSON (a server message may itself contain a brace). The object
|
|
403
404
|
carries scalars first (`code`, revision), then nested objects whole; `HealthFixRefusal.row` is always whole, any other nested
|
package/dist/client.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export declare class ConduytApiError extends Error {
|
|
|
12
12
|
readonly code: "http" | "timeout" | "response_too_large" | "network";
|
|
13
13
|
readonly retryable: boolean;
|
|
14
14
|
/** The response's Retry-After, in seconds, when it sent one (a busy 503 / a 429): surfaced to the agent, never used
|
|
15
|
-
* to replay a mutation. */
|
|
15
|
+
* to replay a mutation. Exception (finalize of the convenience upload tools/commands only): on a 409 upload_in_progress the finalize POST is repeated, at most 4 total attempts, the Retry-After honoured and capped at 10 s, 5 s when absent or unreadable. */
|
|
16
16
|
readonly retryAfterSeconds: number | null;
|
|
17
17
|
constructor(opts: {
|
|
18
18
|
message: string;
|
package/dist/client.js
CHANGED
|
@@ -11,7 +11,7 @@ export class ConduytApiError extends Error {
|
|
|
11
11
|
code;
|
|
12
12
|
retryable;
|
|
13
13
|
/** The response's Retry-After, in seconds, when it sent one (a busy 503 / a 429): surfaced to the agent, never used
|
|
14
|
-
* to replay a mutation. */
|
|
14
|
+
* to replay a mutation. Exception (finalize of the convenience upload tools/commands only): on a 409 upload_in_progress the finalize POST is repeated, at most 4 total attempts, the Retry-After honoured and capped at 10 s, 5 s when absent or unreadable. */
|
|
15
15
|
retryAfterSeconds;
|
|
16
16
|
constructor(opts) {
|
|
17
17
|
super(opts.message, opts.cause !== undefined ? { cause: opts.cause } : undefined);
|
package/dist/register-all.js
CHANGED
|
@@ -33,6 +33,7 @@ import { registerBatchOperationTools } from "./tools/batch-operations.js";
|
|
|
33
33
|
import { registerDncTools } from "./tools/dnc.js";
|
|
34
34
|
import { registerEmailOptOutTools } from "./tools/email-opt-out.js";
|
|
35
35
|
import { registerShortLinkTools } from "./tools/short-links.js";
|
|
36
|
+
import { registerDirectUploadTools } from "./tools/direct-uploads.js";
|
|
36
37
|
import { registerMyProfileTools } from "./tools/my-profile.js";
|
|
37
38
|
import { registerSavedFilterTools } from "./tools/saved-filters.js";
|
|
38
39
|
import { registerDripCampaignTools } from "./tools/drip-campaigns.js";
|
|
@@ -94,6 +95,7 @@ export function registerAllTools(server, client) {
|
|
|
94
95
|
registerDncTools(server, client);
|
|
95
96
|
registerEmailOptOutTools(server, client);
|
|
96
97
|
registerShortLinkTools(server, client);
|
|
98
|
+
registerDirectUploadTools(server, client);
|
|
97
99
|
registerMyProfileTools(server, client);
|
|
98
100
|
registerSavedFilterTools(server, client);
|
|
99
101
|
registerDripCampaignTools(server, client);
|
package/dist/tool-annotations.js
CHANGED
|
@@ -222,6 +222,12 @@ export const TOOL_ANNOTATIONS = {
|
|
|
222
222
|
conduyt_add_email_opt_out: update("Add Email Opt-Out"), // a repeat is refused 409 (already listed by hand) before any write, so it lands on the same state; adding a block is protective
|
|
223
223
|
conduyt_resubscribe_email_opt_out: destroy("Resubscribe Email Address"), // consent change: removing the row re-enables every customer-facing email to the address
|
|
224
224
|
conduyt_get_my_profile: ro("Get My Profile"),
|
|
225
|
+
conduyt_create_file_upload_intent: create("Create File Upload Intent"), // mints a pending upload record and a signed PUT target; each call is a new upload id
|
|
226
|
+
conduyt_finalize_file_upload: update("Finalize File Upload"), // finalizing the same upload again answers the same body
|
|
227
|
+
conduyt_create_chat_upload_intent: create("Create Chat Upload Intent"),
|
|
228
|
+
conduyt_finalize_chat_upload: update("Finalize Chat Upload"), // finalizing again answers the same body
|
|
229
|
+
conduyt_upload_local_file: create("Upload Local File"), // reads a local file and PUTs it to the signed storage URL; each call stores a new file
|
|
230
|
+
conduyt_upload_chat_attachment: create("Upload Chat Attachment"),
|
|
225
231
|
conduyt_create_short_link: update("Create Short Link"), // one link per account and target: a repeat returns the same link (idempotent), nothing existing is changed
|
|
226
232
|
conduyt_list_saved_filters: ro("List Saved Filters"),
|
|
227
233
|
conduyt_create_saved_filter: create("Create Saved Filter"),
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { ConduytClient } from "../client.js";
|
|
3
|
+
/**
|
|
4
|
+
* Direct-to-storage uploads (#565; app routes POST /api/v1/files/upload/intent + /finalize and
|
|
5
|
+
* POST /api/v1/chat/channels/{id}/upload/intent + /finalize). The bytes go straight to the signed storage URL; the API never
|
|
6
|
+
* carries the file. The four routes are exposed one to one, plus ONE convenience tool per surface that runs the whole flow
|
|
7
|
+
* (intent, PUT the bytes of a local file, finalize). Every API call is sent once: a refusal reaches the agent verbatim.
|
|
8
|
+
* The only repeat is finalize on 409 upload_in_progress, which the API documents as retryable (finalizing again answers the
|
|
9
|
+
* same body), and only inside the convenience tools.
|
|
10
|
+
*/
|
|
11
|
+
/** the app's caps (lib/file-upload-policy.ts CRM_FILE_MAX_BYTES, lib/chat-upload-policy.ts CHAT_UPLOAD_MAX_BYTES) */
|
|
12
|
+
export declare const FILE_UPLOAD_MAX_BYTES: number;
|
|
13
|
+
export declare const CHAT_UPLOAD_MAX_BYTES: number;
|
|
14
|
+
export declare const tooLargeMessage: (size: number, max: number) => string;
|
|
15
|
+
/** both app finalize routes set maxDuration = 120 (finalize reads the staged object back and rewrites it), above the client's 30 s default */
|
|
16
|
+
export declare const FINALIZE_TIMEOUT_MS = 120000;
|
|
17
|
+
/** finalize retry on 409 upload_in_progress: exactly `attempts` calls; between them the refusal's Retry-After (capped at maxDelayMs)
|
|
18
|
+
* or, when it sent none or an unreadable one, delayMs. `sleep` is module state so tests can spy on the scheduled delay. */
|
|
19
|
+
export declare const finalizeRetry: {
|
|
20
|
+
attempts: number;
|
|
21
|
+
delayMs: number;
|
|
22
|
+
maxDelayMs: number;
|
|
23
|
+
sleep: (ms: number) => Promise<unknown>;
|
|
24
|
+
};
|
|
25
|
+
/** the milliseconds to wait before finalizing again; pure */
|
|
26
|
+
export declare function finalizeWaitMs(err: unknown): number;
|
|
27
|
+
/** Everything the end-to-end tools do after the argument checks; exported for the tests. */
|
|
28
|
+
export declare function runDirectUpload(client: ConduytClient, o: {
|
|
29
|
+
intentPath: string;
|
|
30
|
+
finalizePath: string;
|
|
31
|
+
maxBytes: number;
|
|
32
|
+
filePath: string;
|
|
33
|
+
fileName?: string;
|
|
34
|
+
contentType?: string;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
content: Array<{
|
|
37
|
+
type: "text";
|
|
38
|
+
text: string;
|
|
39
|
+
}>;
|
|
40
|
+
}>;
|
|
41
|
+
export declare function registerDirectUploadTools(server: McpServer, client: ConduytClient): void;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { formatResult, formatToolError } from "../client.js";
|
|
7
|
+
import { strictInput } from "./strict.js";
|
|
8
|
+
/**
|
|
9
|
+
* Direct-to-storage uploads (#565; app routes POST /api/v1/files/upload/intent + /finalize and
|
|
10
|
+
* POST /api/v1/chat/channels/{id}/upload/intent + /finalize). The bytes go straight to the signed storage URL; the API never
|
|
11
|
+
* carries the file. The four routes are exposed one to one, plus ONE convenience tool per surface that runs the whole flow
|
|
12
|
+
* (intent, PUT the bytes of a local file, finalize). Every API call is sent once: a refusal reaches the agent verbatim.
|
|
13
|
+
* The only repeat is finalize on 409 upload_in_progress, which the API documents as retryable (finalizing again answers the
|
|
14
|
+
* same body), and only inside the convenience tools.
|
|
15
|
+
*/
|
|
16
|
+
/** the app's caps (lib/file-upload-policy.ts CRM_FILE_MAX_BYTES, lib/chat-upload-policy.ts CHAT_UPLOAD_MAX_BYTES) */
|
|
17
|
+
export const FILE_UPLOAD_MAX_BYTES = 50 * 1024 * 1024;
|
|
18
|
+
export const CHAT_UPLOAD_MAX_BYTES = 10 * 1024 * 1024;
|
|
19
|
+
/** the app's own wording (lib/direct-upload.ts mb()) so a client-side refusal reads the same as the server's 413 */
|
|
20
|
+
const mb = (bytes) => `${Math.round((bytes / 1024 / 1024) * 10) / 10} MB`;
|
|
21
|
+
export const tooLargeMessage = (size, max) => `File is too large (${mb(size)}). Maximum allowed: ${mb(max)}.`;
|
|
22
|
+
/** both app finalize routes set maxDuration = 120 (finalize reads the staged object back and rewrites it), above the client's 30 s default */
|
|
23
|
+
export const FINALIZE_TIMEOUT_MS = 120_000;
|
|
24
|
+
/** finalize retry on 409 upload_in_progress: exactly `attempts` calls; between them the refusal's Retry-After (capped at maxDelayMs)
|
|
25
|
+
* or, when it sent none or an unreadable one, delayMs. `sleep` is module state so tests can spy on the scheduled delay. */
|
|
26
|
+
export const finalizeRetry = {
|
|
27
|
+
attempts: 4,
|
|
28
|
+
delayMs: 5000,
|
|
29
|
+
maxDelayMs: 10_000,
|
|
30
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
31
|
+
};
|
|
32
|
+
/** the milliseconds to wait before finalizing again; pure */
|
|
33
|
+
export function finalizeWaitMs(err) {
|
|
34
|
+
const ra = err?.retryAfterSeconds;
|
|
35
|
+
return typeof ra === "number" && Number.isFinite(ra) && ra >= 0 ? Math.min(ra * 1000, finalizeRetry.maxDelayMs) : finalizeRetry.delayMs;
|
|
36
|
+
}
|
|
37
|
+
const unwrap = (r) => (r && typeof r === "object" && "data" in r ? r.data : r);
|
|
38
|
+
const FILE_SCOPE = "an API key needs the 'files' scope and the member files.create";
|
|
39
|
+
const CHAT_SCOPE = "an API key needs the 'chat' scope and the member communications.create and membership of the channel";
|
|
40
|
+
const INTENT_ERRORS = "Errors, surfaced verbatim and never retried: 400 file_name_required / file_type_not_allowed / file_size_required / file_empty; 413 file_too_large; 429 (rate limited, Retry-After in the error); 503 uploads_unavailable (nothing changed).";
|
|
41
|
+
const FINALIZE_ERRORS = "Errors, surfaced verbatim: 400 upload_id_required; 404 upload_not_found (malformed, unknown, expired or someone else's upload: upload again); 409 upload_incomplete (the bytes were never PUT, or not all of them: upload again; not retried); 409 upload_in_progress (still being saved: finalize again, same body); 413 file_too_large (the stored object is over the cap); 422 upload_size_mismatch (the stored size is not the declared one: upload again); 400/415 file_rejected (the content check refused the bytes; the status and message are the check's); 429 (rate limited, Retry-After in the error); 503 uploads_unavailable (try again shortly). Finalize allows 120 seconds.";
|
|
42
|
+
/** Everything the end-to-end tools do after the argument checks; exported for the tests. */
|
|
43
|
+
export async function runDirectUpload(client, o) {
|
|
44
|
+
const abs = resolve(o.filePath);
|
|
45
|
+
let st;
|
|
46
|
+
try {
|
|
47
|
+
st = await stat(abs);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return formatToolError(`Cannot read '${o.filePath}': no such file. Nothing was uploaded.`);
|
|
51
|
+
}
|
|
52
|
+
if (!st.isFile())
|
|
53
|
+
return formatToolError(`'${o.filePath}' is not a regular file. Nothing was uploaded.`);
|
|
54
|
+
// the size cap is checked BEFORE the intent call, with the server's wording
|
|
55
|
+
if (st.size === 0)
|
|
56
|
+
return formatToolError("The file is empty.");
|
|
57
|
+
if (st.size > o.maxBytes)
|
|
58
|
+
return formatToolError(tooLargeMessage(st.size, o.maxBytes));
|
|
59
|
+
const body = { fileName: o.fileName ?? basename(abs), size: st.size };
|
|
60
|
+
if (o.contentType !== undefined)
|
|
61
|
+
body.contentType = o.contentType;
|
|
62
|
+
const intent = unwrap(await client.post(o.intentPath, body));
|
|
63
|
+
// the bytes go to the signed URL as-is: NO Authorization header (the URL is the credential), exactly the declared length and type
|
|
64
|
+
let put;
|
|
65
|
+
try {
|
|
66
|
+
put = await fetch(intent.uploadUrl, {
|
|
67
|
+
method: intent.method || "PUT",
|
|
68
|
+
headers: { "Content-Type": intent.contentType, "Content-Length": String(st.size) },
|
|
69
|
+
body: Readable.toWeb(createReadStream(abs)),
|
|
70
|
+
duplex: "half",
|
|
71
|
+
signal: AbortSignal.timeout(Math.max(1, intent.expiresIn) * 1000),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
return formatToolError(`Storage upload failed before finalize (${err?.message ?? String(err)}). Nothing was saved; call this tool again (the upload id ${intent.uploadId} is abandoned).`);
|
|
76
|
+
}
|
|
77
|
+
if (!put.ok) {
|
|
78
|
+
return formatToolError(`Storage refused the upload (HTTP ${put.status}). Nothing was saved; call this tool again (the upload id ${intent.uploadId} is abandoned).`);
|
|
79
|
+
}
|
|
80
|
+
for (let attempt = 1;; attempt++) {
|
|
81
|
+
try {
|
|
82
|
+
return formatResult(await client.post(o.finalizePath, { uploadId: intent.uploadId }, { timeoutMs: FINALIZE_TIMEOUT_MS }));
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
const inProgress = err?.status === 409 && /upload_in_progress/.test(err.message);
|
|
86
|
+
if (!inProgress || attempt >= finalizeRetry.attempts)
|
|
87
|
+
throw err;
|
|
88
|
+
await finalizeRetry.sleep(finalizeWaitMs(err));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export function registerDirectUploadTools(server, client) {
|
|
93
|
+
const fileNameField = z.string().min(1).describe("File name with its extension, e.g. contract.pdf (the extension decides the allowed type)");
|
|
94
|
+
const contentTypeField = z.string().optional().describe("MIME type; omit to let the API infer it from the file name");
|
|
95
|
+
const sizeField = z.number().int().positive().describe("Exact size of the file in bytes");
|
|
96
|
+
const uploadIdField = z.string().uuid().describe("uploadId from the intent call, after its bytes were PUT");
|
|
97
|
+
const channelField = z.string().uuid().describe("Team Chat channel UUID (you must be a member)");
|
|
98
|
+
strictInput(server.tool("conduyt_create_file_upload_intent", `Step 1 of a direct-to-storage CRM file upload, up to 50 MB (POST /api/v1/files/upload/intent; ${FILE_SCOPE}). Answers 201 { uploadId, uploadUrl, method: "PUT", contentType, size, expiresIn, maxBytes }: PUT exactly 'size' bytes to uploadUrl with Content-Type: contentType within expiresIn seconds (no Authorization header), then call conduyt_finalize_file_upload. This tool does not move bytes; to upload a local file in one step use conduyt_upload_local_file. ${INTENT_ERRORS}`, { fileName: fileNameField, contentType: contentTypeField, size: sizeField }, async ({ fileName, contentType, size }) => formatResult(await client.post("/api/v1/files/upload/intent", { fileName, contentType, size }))));
|
|
99
|
+
strictInput(server.tool("conduyt_finalize_file_upload", `Step 3 of a direct-to-storage CRM file upload (POST /api/v1/files/upload/finalize; ${FILE_SCOPE}). The stored object must be present and exactly the declared size; its bytes are judged by content, then settled. Answers 201 { uploadId, filename, size, contentType}, the same body as the multipart upload; the uploadId is then attached with POST /api/v1/files { entityType, entityId, uploadId }. Finalizing the same upload again answers the same body. ${FINALIZE_ERRORS}`, { uploadId: uploadIdField }, async ({ uploadId }) => formatResult(await client.post("/api/v1/files/upload/finalize", { uploadId }, { timeoutMs: FINALIZE_TIMEOUT_MS }))));
|
|
100
|
+
strictInput(server.tool("conduyt_create_chat_upload_intent", `Step 1 of a direct-to-storage Team Chat attachment, up to 10 MB (POST /api/v1/chat/channels/{id}/upload/intent; ${CHAT_SCOPE}). Answers 201 { uploadId, uploadUrl, method: "PUT", contentType, size, expiresIn, maxBytes }: PUT exactly 'size' bytes to uploadUrl with Content-Type: contentType within expiresIn seconds (no Authorization header), then call conduyt_finalize_chat_upload. This tool does not move bytes; to upload a local file in one step use conduyt_upload_chat_attachment. A deal channel is 404 for a key without the deals scope; a non-member gets 403. ${INTENT_ERRORS}`, { channelId: channelField, fileName: fileNameField, contentType: contentTypeField, size: sizeField }, async ({ channelId, fileName, contentType, size }) => formatResult(await client.post(`/api/v1/chat/channels/${channelId}/upload/intent`, { fileName, contentType, size }))));
|
|
101
|
+
strictInput(server.tool("conduyt_finalize_chat_upload", `Step 3 of a direct-to-storage Team Chat attachment (POST /api/v1/chat/channels/{id}/upload/finalize; ${CHAT_SCOPE}). Answers 201 { url, name, size, type }, the same body as the multipart upload: send it in a message's metadata.attachments. A message that claims an attachment that was never finalized is refused 409 attachment_unavailable. Finalizing again answers the same body. ${FINALIZE_ERRORS}`, { channelId: channelField, uploadId: uploadIdField }, async ({ channelId, uploadId }) => formatResult(await client.post(`/api/v1/chat/channels/${channelId}/upload/finalize`, { uploadId }, { timeoutMs: FINALIZE_TIMEOUT_MS }))));
|
|
102
|
+
strictInput(server.tool("conduyt_upload_local_file", `Upload a file from the machine this server runs on, end to end, as a CRM file (up to 50 MB): checks the size here first, then intent (POST /api/v1/files/upload/intent), PUT of the bytes to the signed URL, finalize (POST /api/v1/files/upload/finalize). ${FILE_SCOPE}. Answers { uploadId, filename, size, contentType }; attach it with POST /api/v1/files { entityType, entityId, uploadId }. A file over the cap is refused here with the API's wording ("File is too large (60 MB). Maximum allowed: 50 MB.") before any call is made; an empty file likewise ("The file is empty."). API refusals are surfaced verbatim and not retried; a storage failure leaves nothing saved, call the tool again. Only a 409 upload_in_progress on finalize is repeated (at most 4 total attempts, waiting the Retry-After, capped at 10 seconds, else 5 seconds). Finalize refusals: ${FINALIZE_ERRORS}`, {
|
|
103
|
+
filePath: z.string().min(1).describe("Path of the local file to upload"),
|
|
104
|
+
fileName: z.string().min(1).optional().describe("Name to store it under; default: the file's own name"),
|
|
105
|
+
contentType: contentTypeField,
|
|
106
|
+
}, async ({ filePath, fileName, contentType }) => runDirectUpload(client, { intentPath: "/api/v1/files/upload/intent", finalizePath: "/api/v1/files/upload/finalize", maxBytes: FILE_UPLOAD_MAX_BYTES, filePath, fileName, contentType })));
|
|
107
|
+
strictInput(server.tool("conduyt_upload_chat_attachment", `Upload a file from the machine this server runs on, end to end, as a Team Chat attachment (up to 10 MB): checks the size here first, then intent (POST /api/v1/chat/channels/{id}/upload/intent), PUT of the bytes, finalize (.../upload/finalize). ${CHAT_SCOPE}. Answers { url, name, size, type }: send it in a message's metadata.attachments. A file over the cap is refused here with the API's wording ("File is too large (12 MB). Maximum allowed: 10 MB.") before any call is made; an empty file likewise ("The file is empty."). API refusals are surfaced verbatim and not retried; a storage failure leaves nothing saved, call the tool again. Only a 409 upload_in_progress on finalize is repeated (at most 4 total attempts, waiting the Retry-After, capped at 10 seconds, else 5 seconds). Finalize refusals: ${FINALIZE_ERRORS}`, {
|
|
108
|
+
channelId: channelField,
|
|
109
|
+
filePath: z.string().min(1).describe("Path of the local file to upload"),
|
|
110
|
+
fileName: z.string().min(1).optional().describe("Name to store it under; default: the file's own name"),
|
|
111
|
+
contentType: contentTypeField,
|
|
112
|
+
}, async ({ channelId, filePath, fileName, contentType }) => runDirectUpload(client, { intentPath: `/api/v1/chat/channels/${channelId}/upload/intent`, finalizePath: `/api/v1/chat/channels/${channelId}/upload/finalize`, maxBytes: CHAT_UPLOAD_MAX_BYTES, filePath, fileName, contentType })));
|
|
113
|
+
}
|
package/package.json
CHANGED
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
{
|
|
124
124
|
"match": "/api/v1/files",
|
|
125
125
|
"status": "planned",
|
|
126
|
-
"reason": "file listing/metadata is agent-useful; upload stays browser-side"
|
|
126
|
+
"reason": "file listing/metadata is agent-useful; the direct-upload routes (POST /files/upload/intent and /finalize) are covered, the multipart upload stays browser-side"
|
|
127
127
|
},
|
|
128
128
|
{
|
|
129
129
|
"match": "GET /api/v1/chat/messages/",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-10-
|
|
3
|
-
"toolCount":
|
|
4
|
-
"totalEndpoints":
|
|
2
|
+
"generatedAt": "2026-10-28T11:47:07.866Z",
|
|
3
|
+
"toolCount": 372,
|
|
4
|
+
"totalEndpoints": 1115,
|
|
5
5
|
"counts": {
|
|
6
|
-
"covered":
|
|
7
|
-
"excluded":
|
|
8
|
-
"planned":
|
|
6
|
+
"covered": 326,
|
|
7
|
+
"excluded": 334,
|
|
8
|
+
"planned": 455,
|
|
9
9
|
"uncategorized": 0
|
|
10
10
|
},
|
|
11
11
|
"matrix": {
|
|
@@ -269,6 +269,10 @@
|
|
|
269
269
|
"status": "excluded",
|
|
270
270
|
"reason": "raw CSV bytes (up to 50k rows) do not belong in a tool result; conduyt_account_exports_list returns the authed downloadPath for API/CLI retrieval"
|
|
271
271
|
},
|
|
272
|
+
"POST /api/v1/account/company-name": {
|
|
273
|
+
"status": "planned",
|
|
274
|
+
"reason": "account readiness/onboarding surfaces"
|
|
275
|
+
},
|
|
272
276
|
"GET /api/v1/account/readiness": {
|
|
273
277
|
"status": "planned",
|
|
274
278
|
"reason": "account readiness/onboarding surfaces"
|
|
@@ -501,6 +505,10 @@
|
|
|
501
505
|
"status": "excluded",
|
|
502
506
|
"reason": "interactive session/password/SSO flows; API keys are the MCP auth model"
|
|
503
507
|
},
|
|
508
|
+
"POST /api/v1/auth/google": {
|
|
509
|
+
"status": "excluded",
|
|
510
|
+
"reason": "interactive session/password/SSO flows; API keys are the MCP auth model"
|
|
511
|
+
},
|
|
504
512
|
"GET /api/v1/auth/google/callback": {
|
|
505
513
|
"status": "excluded",
|
|
506
514
|
"reason": "interactive session/password/SSO flows; API keys are the MCP auth model"
|
|
@@ -545,6 +553,10 @@
|
|
|
545
553
|
"status": "excluded",
|
|
546
554
|
"reason": "interactive session/password/SSO flows; API keys are the MCP auth model"
|
|
547
555
|
},
|
|
556
|
+
"POST /api/v1/auth/microsoft": {
|
|
557
|
+
"status": "excluded",
|
|
558
|
+
"reason": "interactive session/password/SSO flows; API keys are the MCP auth model"
|
|
559
|
+
},
|
|
548
560
|
"GET /api/v1/auth/microsoft/callback": {
|
|
549
561
|
"status": "excluded",
|
|
550
562
|
"reason": "interactive session/password/SSO flows; API keys are the MCP auth model"
|
|
@@ -1350,6 +1362,18 @@
|
|
|
1350
1362
|
"status": "planned",
|
|
1351
1363
|
"reason": "team chat read/post for agents"
|
|
1352
1364
|
},
|
|
1365
|
+
"POST /api/v1/chat/channels/:id/upload/finalize": {
|
|
1366
|
+
"status": "covered",
|
|
1367
|
+
"tools": [
|
|
1368
|
+
"conduyt_finalize_chat_upload"
|
|
1369
|
+
]
|
|
1370
|
+
},
|
|
1371
|
+
"POST /api/v1/chat/channels/:id/upload/intent": {
|
|
1372
|
+
"status": "covered",
|
|
1373
|
+
"tools": [
|
|
1374
|
+
"conduyt_create_chat_upload_intent"
|
|
1375
|
+
]
|
|
1376
|
+
},
|
|
1353
1377
|
"GET /api/v1/chat/floor": {
|
|
1354
1378
|
"status": "planned",
|
|
1355
1379
|
"reason": "team chat read/post for agents"
|
|
@@ -2918,23 +2942,35 @@
|
|
|
2918
2942
|
},
|
|
2919
2943
|
"GET /api/v1/files": {
|
|
2920
2944
|
"status": "planned",
|
|
2921
|
-
"reason": "file listing/metadata is agent-useful; upload stays browser-side"
|
|
2945
|
+
"reason": "file listing/metadata is agent-useful; the direct-upload routes (POST /files/upload/intent and /finalize) are covered, the multipart upload stays browser-side"
|
|
2922
2946
|
},
|
|
2923
2947
|
"POST /api/v1/files": {
|
|
2924
2948
|
"status": "planned",
|
|
2925
|
-
"reason": "file listing/metadata is agent-useful; upload stays browser-side"
|
|
2949
|
+
"reason": "file listing/metadata is agent-useful; the direct-upload routes (POST /files/upload/intent and /finalize) are covered, the multipart upload stays browser-side"
|
|
2926
2950
|
},
|
|
2927
2951
|
"DELETE /api/v1/files": {
|
|
2928
2952
|
"status": "planned",
|
|
2929
|
-
"reason": "file listing/metadata is agent-useful; upload stays browser-side"
|
|
2953
|
+
"reason": "file listing/metadata is agent-useful; the direct-upload routes (POST /files/upload/intent and /finalize) are covered, the multipart upload stays browser-side"
|
|
2930
2954
|
},
|
|
2931
2955
|
"GET /api/v1/files/:id/download": {
|
|
2932
2956
|
"status": "planned",
|
|
2933
|
-
"reason": "file listing/metadata is agent-useful; upload stays browser-side"
|
|
2957
|
+
"reason": "file listing/metadata is agent-useful; the direct-upload routes (POST /files/upload/intent and /finalize) are covered, the multipart upload stays browser-side"
|
|
2934
2958
|
},
|
|
2935
2959
|
"POST /api/v1/files/upload": {
|
|
2936
2960
|
"status": "planned",
|
|
2937
|
-
"reason": "file listing/metadata is agent-useful; upload stays browser-side"
|
|
2961
|
+
"reason": "file listing/metadata is agent-useful; the direct-upload routes (POST /files/upload/intent and /finalize) are covered, the multipart upload stays browser-side"
|
|
2962
|
+
},
|
|
2963
|
+
"POST /api/v1/files/upload/finalize": {
|
|
2964
|
+
"status": "covered",
|
|
2965
|
+
"tools": [
|
|
2966
|
+
"conduyt_finalize_file_upload"
|
|
2967
|
+
]
|
|
2968
|
+
},
|
|
2969
|
+
"POST /api/v1/files/upload/intent": {
|
|
2970
|
+
"status": "covered",
|
|
2971
|
+
"tools": [
|
|
2972
|
+
"conduyt_create_file_upload_intent"
|
|
2973
|
+
]
|
|
2938
2974
|
},
|
|
2939
2975
|
"GET /api/v1/generated-documents": {
|
|
2940
2976
|
"status": "planned",
|