conduyt-mcp 4.105.6 → 4.105.8

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 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
- ## 392 Tools
5
+ ## 393 Tools
6
6
 
7
7
  ### Discovery
8
8
  | Tool | Description |
@@ -270,18 +270,19 @@ The Mailchimp OAuth connect and callback are browser-only: connect Mailchimp in
270
270
 
271
271
  The thread view (#684): `conduyt_list_chat_messages` takes `topLevel: true` (top-level messages with their reply chip `thread`, and the replies shown in the channel with `threadReply` `{ reason: aiFirstAnswer | alsoInChannel, root }`; never with `afterSeq`) and `conduyt_reply_chat_message` takes `alsoInChannel: true` ("Also send to #channel" on a person's reply). Both are sent only when true. They and the two thread tools work only while the workspace's switch is on (`conduyt_list_chat_channels` then carries `teamChatThreads: true`); while it is off each answers an error result with `code: team_chat_threads_off` and the sentence "The thread view is not switched on for this workspace (account setting teamChatThreads)." Against a server that predates the thread view: the two thread tools answer a 404 error result that says so, `topLevel` is reported as not applied (an error result, never a page of every reply), and an `alsoInChannel` reply is posted in the thread only, with a note saying so (never re-sent).
272
272
 
273
- AI teammate turns (#802, the app listener's #655 phase 1): a turn run through Codex (`conduyt agent listen --adapter codex`) carries the thread so far and, when the channel is linked, "This channel is linked to deal <id>" (read it with `conduyt_get_chat_linked_record`), and ends with exactly ONE of `conduyt_reply_chat_message` (with `idempotencyKey` + `claimToken`), `conduyt_react_chat_message` (with `claimToken`) or `conduyt_stay_silent`; a second, different one is refused 409 `turn_finished`. The inbox itself (`/chat/agents/me/*`: the event's `context.recent`, `channel.dealId` / `contactId` and the page's `budget`) is the listener's protocol and has no tool; `GET /chat/agents` (with each teammate's `turnsToday` / `dailyBudget`) is session-only, refused to every API key, so it has no tool either: the daily budget is visible in Settings > AI and in the listener's log.
273
+ AI teammate turns (#802, the app listener's #655 phase 1): a turn run through Codex (`conduyt agent listen --adapter codex`) carries the thread so far and, when the channel is linked, "This channel is linked to deal <id>" (read it with `conduyt_get_chat_linked_record`), and ends with exactly ONE of `conduyt_reply_chat_message` (with `idempotencyKey` + `claimToken`), `conduyt_react_chat_message` (with `claimToken`) or `conduyt_stay_silent`; a second, different one is refused 409 `turn_finished`. The inbox itself (`/chat/agents/me/*`: the event's `context.recent`, `channel.dealId` / `contactId` and the page's `budget`) is the listener's protocol and has no tool; (#1153) `GET /chat/agents` answers an owner's or admin's key the teammate rows of `conduyt_list_ai_teammates` (no `turnsToday` / `dailyBudget`: those are the Settings session's): the daily budget is visible in Settings > AI and in the listener's log.
274
274
 
275
275
  ### A Grok Bot as an AI teammate (#1140)
276
276
  | Tool | Description |
277
277
  |------|-------------|
278
+ | `conduyt_list_ai_teammates` | (#1153) The workspace's AI teammates (`GET /chat/agents`, an owner's or admin's key, the read tier is enough): `[{ id, handle, name, provider, state, connected, connection, createdAt }]`, nothing secret. Name a teammate to the tools below by its id or its @handle |
278
279
  | `conduyt_connect_ai_teammate` | Step 1 (`POST /chat/agents/{id}/connect`): the message to paste into Grok Bot and the teammate's token, returned ONCE (`{ token, tokenPrefix, message, toolsUrl, agent }`); the token goes into Grok Bot's masked connector field, never into a chat. Every call revokes the earlier tokens. The calling key needs the write tier and the chat and deals scopes |
279
280
  | `conduyt_set_ai_teammate_webhook` | Step 3 (`PUT /chat/agents/{id}/webhook`): `paste` (the block Grok Bot shows) OR `address` + `key`; checked, stored (the key encrypted), tested at once: `{ stored, test, status }`. The key is write-only: never returned, and any returned text has it replaced by `[hidden]`. A test that was not accepted is an error result with its one sentence (`key_refused`, `routine_gone`, `unreachable`, `not_accepted`, `rate_limited`; `teammate_not_live` is a 409 before any test) |
280
281
  | `conduyt_test_ai_teammate_webhook` | The card's Test button (`POST /chat/agents/{id}/webhook/test`): the hello again to the stored routine; `{ test, status }` |
281
282
  | `conduyt_get_ai_teammate_webhook` | The status the card polls (`GET /chat/agents/{id}/webhook`): `state` not_connected / waiting_for_hello / listening / not_listening, `problem { code, message }`, the address masked; poll it after a test until listening |
282
283
  | `conduyt_disconnect_ai_teammate` | `DELETE /chat/agents/{id}/webhook`: tokens revoked, the routine's key cleared, pending deliveries cancelled; the teammate and its messages stay; never blocked by billing |
283
284
 
284
- Owners and admins, with a person's API key; an AI teammate's own key is refused. The teammate is named by its id (a UUID): the teammate list is session-only, so an API key cannot look up a handle. Against a server without these routes (an HTML 404) each tool answers "This Conduyt server does not have the Grok Bot connection yet (it predates it); connect the teammate in Settings > AI instead."
285
+ Owners and admins, with a person's API key; an AI teammate's own key is refused. The teammate is named by its id (a UUID) or its @handle (`conduyt_list_ai_teammates` lists both; the server resolves the handle). Against a server without these routes (an HTML 404) each tool answers "This Conduyt server does not have the Grok Bot connection yet (it predates it); connect the teammate in Settings > AI instead."
285
286
 
286
287
  The remaining Team Chat mutations (edit/delete, pins, claim/handoff and typing/presence writes) are not wrapped yet; sends, thread replies, reactions and direct uploads are wrapped. `GET /chat/messages/{messageId}/location` is a UI deep-link helper and deliberately not wrapped. `GET /cron/team-chat-digest` is a CRON_SECRET-gated job and not key-callable.
287
288
 
@@ -192,6 +192,8 @@ export const TOOL_ANNOTATIONS = {
192
192
  conduyt_get_chat_thread: update("Get Chat Thread"),
193
193
  // (#1140 A3) the Grok Bot teammate connect tools: connect mints a token and revokes the earlier ones on every call; set stores the
194
194
  // routine key (replacing the last) and sends a test each call; test sends one; disconnect revokes the tokens
195
+ // (#1153) the teammate list: a read
196
+ conduyt_list_ai_teammates: ro("List AI Teammates"),
195
197
  conduyt_connect_ai_teammate: row("Connect AI Teammate", false, true, false),
196
198
  conduyt_set_ai_teammate_webhook: row("Set AI Teammate Webhook", false, false, false),
197
199
  conduyt_test_ai_teammate_webhook: create("Test AI Teammate Webhook"),
@@ -14,7 +14,9 @@ export { hide } from "./routine-key-redaction.js";
14
14
  */
15
15
  export declare const OLDER_SERVER_SENTENCE = "This Conduyt server does not have the Grok Bot connection yet (it predates it); connect the teammate in Settings > AI instead.";
16
16
  export declare const AI_OFF_SENTENCE = "AI teammates are switched off for this workspace; an owner or admin turns them on in Settings > AI.";
17
- export declare const NOT_FOUND_SENTENCE = "There is no AI teammate with that id in this workspace (it may have been revoked).";
17
+ export declare const NOT_FOUND_SENTENCE = "There is no AI teammate with that id or handle in this workspace (it may have been revoked); conduyt_list_ai_teammates lists them.";
18
+ /** (#1153) GET /chat/agents on a server from before #1153 refuses every API key with its session-only sentence */
19
+ export declare const OLDER_LIST_SENTENCE = "This Conduyt server does not let an API key read the AI teammate list yet (it predates it); see the teammates in Settings > AI.";
18
20
  export declare const TEAMMATE_KEY_SENTENCE = "This is an AI teammate's own key; connect a Grok Bot with a person's API key (an owner or admin).";
19
21
  export declare const NO_CREDENTIALS_SENTENCE = "Give the routine's address and key: paste (the block Grok Bot shows), or address and key.";
20
22
  /** the one sentence for a refused call, and its code when the API sent one */
@@ -16,7 +16,9 @@ export { hide } from "./routine-key-redaction.js";
16
16
  */
17
17
  export const OLDER_SERVER_SENTENCE = "This Conduyt server does not have the Grok Bot connection yet (it predates it); connect the teammate in Settings > AI instead.";
18
18
  export const AI_OFF_SENTENCE = "AI teammates are switched off for this workspace; an owner or admin turns them on in Settings > AI.";
19
- export const NOT_FOUND_SENTENCE = "There is no AI teammate with that id in this workspace (it may have been revoked).";
19
+ export const NOT_FOUND_SENTENCE = "There is no AI teammate with that id or handle in this workspace (it may have been revoked); conduyt_list_ai_teammates lists them.";
20
+ /** (#1153) GET /chat/agents on a server from before #1153 refuses every API key with its session-only sentence */
21
+ export const OLDER_LIST_SENTENCE = "This Conduyt server does not let an API key read the AI teammate list yet (it predates it); see the teammates in Settings > AI.";
20
22
  export const TEAMMATE_KEY_SENTENCE = "This is an AI teammate's own key; connect a Grok Bot with a person's API key (an owner or admin).";
21
23
  export const NO_CREDENTIALS_SENTENCE = "Give the routine's address and key: paste (the block Grok Bot shows), or address and key.";
22
24
  function errorDetailsOf(message) {
@@ -89,10 +91,34 @@ function testOutcome(secrets) {
89
91
  return connectError(problem?.message ?? "Grok Bot did not accept the test.", secrets, { status: 200, ...(problem?.code ? { code: problem.code } : {}), answer: d });
90
92
  };
91
93
  }
92
- const TEAMMATE_ID = z.string().uuid().describe("The Grok Bot teammate's id (a UUID: the agent.id of conduyt_connect_ai_teammate's answer, or the senderId of its messages). A handle cannot be looked up with an API key (the teammate list is session-only)");
94
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
95
+ /** the app's handle rule (lib/agent-auth.ts normalizeAgentHandle): a letter, then 1-31 letters, digits, _ or -, case-insensitive */
96
+ const HANDLE_RE = /^@[a-z][a-z0-9_-]{1,31}$/i;
97
+ /** (#1153) the teammate: its id (a UUID) or its @handle, lower-cased; the server resolves the handle (lib/agent-webhook-routes.ts
98
+ * parseTeammateRef, the one place for every surface), so nothing here looks it up */
99
+ const TEAMMATE_ID = z.string().trim().refine((v) => UUID_RE.test(v) || HANDLE_RE.test(v), { message: "teammateId must be the teammate's id (a UUID) or its @handle" })
100
+ .describe("The Grok Bot teammate: its id (a UUID) or its @handle, as conduyt_list_ai_teammates lists them (the agent.id of conduyt_connect_ai_teammate's answer and the senderId of its messages are the id too)");
101
+ /** the path segment of a teammate: trimmed and lower-cased (handles and ids are case-insensitive), an @handle travels as %40handle */
102
+ const seg = (teammateId) => encodeURIComponent(teammateId.trim().toLowerCase());
93
103
  const WHO = "Owners and admins, with a person's API key (the chat scope); an AI teammate's own key is refused (403 agent_route_refused). The teammate must have the provider Grok Bot (409 agent_not_webhook_provider otherwise). Answers 404 ai_teammates_off while AI teammates are switched off and 402 plan_required when the workspace is not entitled. Against a server that predates these routes (an HTML 404) the error says so.";
94
104
  export function registerAiTeammateConnectTools(server, client) {
95
- strictInput(server.tool("conduyt_connect_ai_teammate", `Step 1 of connecting a Grok Bot as an AI teammate (POST /api/v1/chat/agents/{id}/connect). Mints the teammate's token and returns it ONCE: { token, tokenPrefix, message, toolsUrl, agent }. Give the person the message to paste into Grok Bot and the token to paste into Grok Bot's masked connector field; NEVER post the token in Team Chat or any message, and never call this again just to read it: every call revokes the teammate's earlier tokens (the card's "Get a new token"). The calling key must hold the write tier and the chat and deals scopes (403 api_key_scope_insufficient otherwise). 409 agent_listener_webhook_conflict while the teammate's listener is running; 409 teammate_not_live when its owner lost access. Step 3 is conduyt_set_ai_teammate_webhook. ${WHO}`, { teammateId: TEAMMATE_ID }, async ({ teammateId }) => attempt([], () => client.post(`/api/v1/chat/agents/${teammateId}/connect`, {}), (res) => connectResult(unwrap(res), []))));
105
+ // (#1153, tri-surface parity) the workspace's AI teammates, so an agent can find the one to connect by id or @handle
106
+ strictInput(server.tool("conduyt_list_ai_teammates", "List the workspace's live AI teammates (GET /api/v1/chat/agents; a removed teammate is not listed), oldest first: [{ id, handle, name, provider (claude_code, codex, grok), state (listening, replying, needs_attention (#1213: listening, nothing running, and a turn in the last 24 hours failed or timed out), away), connected (a Grok Bot teammate: its connection is stored and live; any other: a listener runs now), connection (a Grok Bot teammate's not_connected, waiting_for_hello, listening or not_listening; null otherwise), knowledgeFolders (#1209: how many knowledge folders the teammate's RUNNING listeners reported, the sum over the listeners whose heartbeat beats now, 0 = none; a knowledge folder is a folder on the listener's computer the teammate reads on every turn, whoever asked, read-only, secrets-looking files excluded; it is chosen on that computer with \`conduyt agent listen\` (the start directory, --knowledge <folder> or --no-knowledge), never through Conduyt, and its path never leaves the computer), createdAt }]. Nothing secret is listed. Name a teammate to the Grok Bot tools by its id or its @handle. Owners and admins, with a person's API key (the chat scope; reading is enough); a member's key is refused (403) and an AI teammate's own key too (403 agent_route_refused). Adding a teammate stays in Settings > AI. Answers 404 ai_teammates_off while AI teammates are switched off and 402 plan_required when the workspace is not entitled; a workspace with more than 200 live teammates is refused to an API key (413 agent_list_over_ceiling; Settings > AI lists them all).", {}, async () => {
107
+ try {
108
+ const res = await client.get("/api/v1/chat/agents");
109
+ return connectResult(unwrap(res), []);
110
+ }
111
+ catch (err) {
112
+ if (err instanceof ConduytApiError && err.code === "http") {
113
+ if (err.status === 403 && /interactive browser session/i.test(err.message))
114
+ return connectError(OLDER_LIST_SENTENCE, [], { status: 403 });
115
+ const r = refusalOf(err);
116
+ return connectError(r.sentence, [], { status: r.status, ...(r.code ? { code: r.code } : {}) });
117
+ }
118
+ throw err;
119
+ }
120
+ }));
121
+ strictInput(server.tool("conduyt_connect_ai_teammate", `Step 1 of connecting a Grok Bot as an AI teammate (POST /api/v1/chat/agents/{id}/connect). Mints the teammate's token and returns it ONCE: { token, tokenPrefix, message, toolsUrl, agent }. Give the person the message to paste into Grok Bot and the token to paste into Grok Bot's masked connector field; NEVER post the token in Team Chat or any message, and never call this again just to read it: every call revokes the teammate's earlier tokens (the card's "Get a new token"). The calling key must hold the write tier and the chat and deals scopes (403 api_key_scope_insufficient otherwise). 409 agent_listener_webhook_conflict while the teammate's listener is running; 409 teammate_not_live when its owner lost access. Step 3 is conduyt_set_ai_teammate_webhook. ${WHO}`, { teammateId: TEAMMATE_ID }, async ({ teammateId }) => attempt([], () => client.post(`/api/v1/chat/agents/${seg(teammateId)}/connect`, {}), (res) => connectResult(unwrap(res), []))));
96
122
  strictInput(server.tool("conduyt_set_ai_teammate_webhook", `Step 3 of connecting a Grok Bot (PUT /api/v1/chat/agents/{id}/webhook): the routine's address and key, as Grok Bot shows them. Send EITHER paste (the whole block the person copied: it must hold one https address and one key) OR address and key. Conduyt checks the address (https, a public host), stores both (the key encrypted), sends the test at once (10 s) and answers { stored, test: { ok, statusCode, runUuid, problem }, status }. The key is write-only: it is never returned, and any text this tool returns has it replaced by [hidden]. A test that was not accepted is an error result { ok: false, error: <its one sentence>, status: 200, code (key_refused, routine_gone, unreachable, not_accepted, rate_limited), answer: <the whole answer> }; an accepted test leaves status Waiting for Grok Bot to say hello: poll conduyt_get_ai_teammate_webhook (every few seconds, up to about 3 minutes) until state is listening or a problem is named. Refusals: 400 with the input codes (input_conflict, address_missing, address_invalid, address_ambiguous, key_missing, key_invalid, key_ambiguous), 422 address_invalid / unreachable, 409 agent_token_missing (run conduyt_connect_ai_teammate first), agent_listener_webhook_conflict, agent_webhook_contended, teammate_not_live, 429 after 20 tests per teammate in 10 minutes, 503 while a delivery is in flight. ${WHO}`, {
97
123
  teammateId: TEAMMATE_ID,
98
124
  paste: z.string().min(1).max(12_000).optional().describe("The block Grok Bot shows (address and key together); not with address or key"),
@@ -105,9 +131,9 @@ export function registerAiTeammateConnectTools(server, client) {
105
131
  if (paste === undefined && address === undefined)
106
132
  return connectError(NO_CREDENTIALS_SENTENCE, secrets, { status: null, code: "address_missing" });
107
133
  // the body as ONE literal: the contract gate reads the request fields (paste, address, key) from it
108
- return attempt(secrets, () => client.put(`/api/v1/chat/agents/${teammateId}/webhook`, { ...(paste !== undefined ? { paste } : {}), ...(address !== undefined ? { address } : {}), ...(key !== undefined ? { key } : {}) }), testOutcome(secrets));
134
+ return attempt(secrets, () => client.put(`/api/v1/chat/agents/${seg(teammateId)}/webhook`, { ...(paste !== undefined ? { paste } : {}), ...(address !== undefined ? { address } : {}), ...(key !== undefined ? { key } : {}) }), testOutcome(secrets));
109
135
  }));
110
- strictInput(server.tool("conduyt_test_ai_teammate_webhook", `The card's Test button (POST /api/v1/chat/agents/{id}/webhook/test): sends the hello again to the stored routine (nothing is pasted); an accepted test turns a stored connection live. Answers { test, status } like conduyt_set_ai_teammate_webhook (a test that was not accepted is an error result with its sentence); then poll conduyt_get_ai_teammate_webhook for the hello. 409 agent_webhook_not_connected when nothing is stored; 429 after 20 tests per teammate in 10 minutes. ${WHO}`, { teammateId: TEAMMATE_ID }, async ({ teammateId }) => attempt([], () => client.post(`/api/v1/chat/agents/${teammateId}/webhook/test`, {}), testOutcome([]))));
111
- strictInput(server.tool("conduyt_get_ai_teammate_webhook", `The Grok Bot connection's status (GET /api/v1/chat/agents/{id}/webhook), what the Settings card polls: { state (not_connected | waiting_for_hello | listening | not_listening), label, problem: { code, message } | null (the sentence of what to fix), waitingUntil, connected, address (masked), lastAttempt, tokenUsedAt, lastMessageAt }. Never carries the key. ${WHO}`, { teammateId: TEAMMATE_ID }, async ({ teammateId }) => attempt([], () => client.get(`/api/v1/chat/agents/${teammateId}/webhook`), (res) => connectResult(unwrap(res), []))));
112
- strictInput(server.tool("conduyt_disconnect_ai_teammate", `Disconnect a Grok Bot teammate (DELETE /api/v1/chat/agents/{id}/webhook; never blocked by billing): its tokens are revoked, the routine's key is cleared and pending deliveries are cancelled; the teammate and its messages stay. Answers { disconnected, revokedKeys, webhook, cancelledDeliveries }. Safe to repeat. Connect it again with conduyt_connect_ai_teammate then conduyt_set_ai_teammate_webhook. ${WHO.replace(" and 402 plan_required when the workspace is not entitled", "")}`, { teammateId: TEAMMATE_ID }, async ({ teammateId }) => attempt([], () => client.del(`/api/v1/chat/agents/${teammateId}/webhook`), (res) => connectResult(unwrap(res), []))));
136
+ strictInput(server.tool("conduyt_test_ai_teammate_webhook", `The card's Test button (POST /api/v1/chat/agents/{id}/webhook/test): sends the hello again to the stored routine (nothing is pasted); an accepted test turns a stored connection live. Answers { test, status } like conduyt_set_ai_teammate_webhook (a test that was not accepted is an error result with its sentence); then poll conduyt_get_ai_teammate_webhook for the hello. 409 agent_webhook_not_connected when nothing is stored; 429 after 20 tests per teammate in 10 minutes. ${WHO}`, { teammateId: TEAMMATE_ID }, async ({ teammateId }) => attempt([], () => client.post(`/api/v1/chat/agents/${seg(teammateId)}/webhook/test`, {}), testOutcome([]))));
137
+ strictInput(server.tool("conduyt_get_ai_teammate_webhook", `The Grok Bot connection's status (GET /api/v1/chat/agents/{id}/webhook), what the Settings card polls: { state (not_connected | waiting_for_hello | listening | not_listening), label, problem: { code, message } | null (the sentence of what to fix), waitingUntil, connected, address (masked), lastAttempt, tokenUsedAt, lastMessageAt }. Never carries the key. ${WHO}`, { teammateId: TEAMMATE_ID }, async ({ teammateId }) => attempt([], () => client.get(`/api/v1/chat/agents/${seg(teammateId)}/webhook`), (res) => connectResult(unwrap(res), []))));
138
+ strictInput(server.tool("conduyt_disconnect_ai_teammate", `Disconnect a Grok Bot teammate (DELETE /api/v1/chat/agents/{id}/webhook; never blocked by billing): its tokens are revoked, the routine's key is cleared and pending deliveries are cancelled; the teammate and its messages stay. Answers { disconnected, revokedKeys, webhook, cancelledDeliveries }. Safe to repeat. Connect it again with conduyt_connect_ai_teammate then conduyt_set_ai_teammate_webhook. ${WHO.replace(" and 402 plan_required when the workspace is not entitled", "")}`, { teammateId: TEAMMATE_ID }, async ({ teammateId }) => attempt([], () => client.del(`/api/v1/chat/agents/${seg(teammateId)}/webhook`), (res) => connectResult(unwrap(res), []))));
113
139
  }
@@ -129,7 +129,7 @@ export function registerChatTools(server, client) {
129
129
  return formatToolError("This Conduyt server did not apply topLevel: its messages carry no thread summaries (it predates the thread view). Read the channel without topLevel.");
130
130
  return formatResult(page);
131
131
  }));
132
- strictInput(server.tool("conduyt_send_chat_message", "Send a Team Chat message as the API key's user (POST /api/v1/chat/channels/{id}/messages). The caller must be a channel member and have the communications:create permission. AI-agent keys are subject to the server's six-consecutive-agent-turn cap; a 409 agent_human_required means no message was sent (markerCreated: true on the refusal that inserted the one human-handoff line, false while the thread stays paused). A 409 agents_muted_in_channel means an admin turned \"agents respond here\" off for this channel: the post was refused, nothing was written, and it must not be retried (same shape as agent_human_required). body is plain Team Chat text (1-10,000 characters).", {
132
+ strictInput(server.tool("conduyt_send_chat_message", "Send a Team Chat message as the API key's user (POST /api/v1/chat/channels/{id}/messages). The caller must be a channel member and have the communications:create permission. AI-agent keys are subject to the server's six-consecutive-agent-turn cap; a 409 agent_human_required means no message was sent (markerCreated: true on the refusal that inserted the one human-handoff line, false while the thread stays paused). A 409 agents_muted_in_channel means an admin turned \"agents respond here\" off for this channel: the post was refused, nothing was written, and it must not be retried (same shape as agent_human_required). (#1213) A 409 write_outside_turn means this AI teammate is answering a turn (it holds a running claim): every post must be that turn's reply, in its thread, with conduyt_reply_chat_message and the turn's idempotencyKey + claimToken; nothing was written, do not retry. body is plain Team Chat text (1-10,000 characters).", {
133
133
  channelId: z.string().uuid().describe("Channel UUID"),
134
134
  body: z.string().trim().min(1).max(10_000).describe("Message text"),
135
135
  idempotencyKey: z.string().regex(/^[A-Za-z0-9:_.-]{1,128}$/).optional().describe("Paired AI agent keys only: a key unique per agent and source turn (the listener uses reply:<source message id>). The same key returns the reply already created (200, idempotentReplay: true) instead of posting a second one; a key used in another channel is a 409 idempotency_key_reused. A person's key gets 400."),
@@ -137,14 +137,19 @@ export function registerChatTools(server, client) {
137
137
  const result = await client.post(`/api/v1/chat/channels/${channelId}/messages`, { body, ...(idempotencyKey ? { idempotencyKey } : {}) });
138
138
  return formatResult(result);
139
139
  }));
140
- strictInput(server.tool("conduyt_reply_chat_message", "Reply in a Team Chat thread as the API key's user (POST /api/v1/chat/channels/{id}/messages with parentId). parentId must name a live top-level message in that channel. The caller must be a member with communications:create. AI-agent keys are subject to the same six-consecutive-agent-turn cap as a top-level send, and to the same 409 agent_human_required / agents_muted_in_channel refusals (nothing written, do not retry; agents_muted_in_channel = an admin turned \"agents respond here\" off for the channel).", {
140
+ strictInput(server.tool("conduyt_reply_chat_message", "Reply in a Team Chat thread as the API key's user (POST /api/v1/chat/channels/{id}/messages with parentId). parentId must name a live top-level message in that channel. The caller must be a member with communications:create. AI-agent keys are subject to the same six-consecutive-agent-turn cap as a top-level send, and to the same 409 agent_human_required / agents_muted_in_channel refusals (nothing written, do not retry; agents_muted_in_channel = an admin turned \"agents respond here\" off for the channel). (#1213) AI teammates may talk to each other in a thread, bounded: with two consecutive AI posts already (the asker and the answerer), a turn answering an AI teammate's message is refused 409 agent_human_required with reason agent_chain_cap (the same one pause line; the thread waits for a person). A 409 parent_deleted means the parent, its thread or the turn's message was deleted: nothing was written and the turn ended; do not retry. (#1213) While an AI teammate is answering a turn (it holds a running claim) every post must be that turn's reply (its reply:<source> key and claim token, in the source's channel and thread): anything else is refused 409 write_outside_turn, nothing posted. A Grok Bot turn's answer (idempotencyKey turn:<message id>) needs turnToken (409 turn_token_invalid otherwise).", {
141
141
  channelId: z.string().uuid().describe("Channel UUID"),
142
142
  parentId: z.string().uuid().describe("Top-level message UUID to reply to"),
143
143
  body: z.string().trim().min(1).max(10_000).describe("Reply text"),
144
144
  idempotencyKey: z.string().regex(/^[A-Za-z0-9:_.-]{1,128}$/).optional().describe("Paired AI agent keys only: a key unique per agent and source turn (the listener uses reply:<source message id>). The same key returns the reply already created (200, idempotentReplay: true) instead of posting a second one; a key used in another channel is a 409 idempotency_key_reused. A person's key gets 400."),
145
145
  alsoInChannel: z.boolean().optional().describe("(#684, a person's reply, only while the workspace's thread view is on) true = \"Also send to #channel\": the ONE reply is also shown in the channel (topLevel reads list it with threadReply.reason alsoInChannel); never a second message or notification. Sent only when true. 409 team_chat_threads_off while the thread view is off (nothing posted); an AI teammate never sets it (its first answer already shows in the channel): refused here with claimToken, 400 from the server otherwise"),
146
146
  claimToken: z.string().regex(/^[1-9][0-9]{0,8}:[A-Za-z0-9:_.-]{8,128}$/).optional().describe("Paired AI agent keys only; REQUIRED with idempotencyKey reply:<source message id> (refused otherwise, nothing posted): the listener's claim token <generation>:<listener id> from the turn prompt. The reply completes that turn only when posted in the source's channel and thread with the current generation's token; a reply:<source> reply anywhere else, or with a stale token, is refused 409 (reply_wrong_channel / reply_wrong_thread / reply_stale_claim) with nothing posted, so a corrected retry still completes the turn."),
147
- }, async ({ channelId, parentId, body, idempotencyKey, claimToken, alsoInChannel }) => {
147
+ turnToken: z.string().regex(/^[1-9][0-9]{9,15}\.[0-9a-f]{64}$/).optional().describe("(#1213) an AI teammate answering a Grok Bot turn: the turn's turnToken from its payload; REQUIRED with idempotencyKey turn:<message id> (refused otherwise, nothing posted)"),
148
+ }, async ({ channelId, parentId, body, idempotencyKey, claimToken, turnToken, alsoInChannel }) => {
149
+ // (#1213) a Grok Bot turn's answer always carries the turn's server-minted token
150
+ if (!turnToken && typeof idempotencyKey === "string" && /^turn:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(idempotencyKey)) {
151
+ throw new Error("turnToken is required with an idempotencyKey turn:<message id> (the turn's turnToken from its payload); nothing was posted");
152
+ }
148
153
  // (#599 r7) a reply to an inbox turn (idempotencyKey reply:<source message id>) always names its claim
149
154
  if (!claimToken && typeof idempotencyKey === "string" && /^reply:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(idempotencyKey)) {
150
155
  throw new Error("claimToken is required with an idempotencyKey reply:<source message id> (the turn prompt's reply claim token); nothing was posted");
@@ -155,7 +160,7 @@ export function registerChatTools(server, client) {
155
160
  const also = alsoInChannel === true;
156
161
  let result;
157
162
  try {
158
- result = await client.post(`/api/v1/chat/channels/${channelId}/messages`, { body, parentId, ...(idempotencyKey ? { idempotencyKey } : {}), ...(claimToken ? { claimToken } : {}), ...(also ? { alsoInChannel: true } : {}) });
163
+ result = await client.post(`/api/v1/chat/channels/${channelId}/messages`, { body, parentId, ...(idempotencyKey ? { idempotencyKey } : {}), ...(claimToken ? { claimToken } : {}), ...(turnToken ? { turnToken } : {}), ...(also ? { alsoInChannel: true } : {}) });
159
164
  }
160
165
  catch (err) {
161
166
  const refused = also ? threadsRefusal(err, { offSentence: `${THREADS_OFF_SENTENCE} Nothing was posted: send it again without alsoInChannel.` }) : null;
@@ -167,20 +172,22 @@ export function registerChatTools(server, client) {
167
172
  const out = formatResult(result);
168
173
  return note ? { ...out, content: [...out.content, { type: "text", text: note }] } : out;
169
174
  }));
170
- strictInput(server.tool("conduyt_react_chat_message", "Add a reaction to a Team Chat message as the API key's user (POST /api/v1/chat/channels/{id}/messages/{messageId}/reactions). The server accepts only its Team Chat reaction set and returns the fully hydrated message. Repeating the same reaction is safe and leaves one reaction from this user. (#802, app #655 c) An AI teammate ENDS its turn with a reaction by passing claimToken (the turn prompt's reply claim token): the reaction then completes that turn (outcome reacted) in the same transaction, so the turn needs no reply. React to the turn's message or one in its thread; when it is another message of the thread, pass the turn's message id as sourceMessageId. Exactly one of a reply, a reaction or conduyt_stay_silent ends a turn: a token that is not the running holder's, or a message outside the turn's thread, is refused 409 reaction_stale_claim, and a turn that already ended (a reply, a silence, another reaction) 409 turn_finished with its outcome; nothing is written on either, do not retry. A person's key sending claimToken gets 400.", {
175
+ strictInput(server.tool("conduyt_react_chat_message", "Add a reaction to a Team Chat message as the API key's user (POST /api/v1/chat/channels/{id}/messages/{messageId}/reactions). The server accepts only its Team Chat reaction set and returns the fully hydrated message. Repeating the same reaction is safe and leaves one reaction from this user. (#802, app #655 c) An AI teammate ENDS its turn with a reaction by passing claimToken (the turn prompt's reply claim token): the reaction then completes that turn (outcome reacted) in the same transaction, so the turn needs no reply. React to the turn's message or one in its thread; when it is another message of the thread, pass the turn's message id as sourceMessageId. Exactly one of a reply, a reaction or conduyt_stay_silent ends a turn: a token that is not the running holder's, or a message outside the turn's thread, is refused 409 reaction_stale_claim, and a turn that already ended (a reply, a silence, another reaction) 409 turn_finished with its outcome; nothing is written on either, do not retry. A person's key sending claimToken gets 400. (#1213 r3) While the teammate is answering a turn its only reaction is that turn's (409 write_outside_turn otherwise); a Grok Bot teammate passes its turn's turnToken (and sourceMessageId for another message of the turn's thread).", {
171
176
  channelId: z.string().uuid().describe("Channel UUID"),
172
177
  messageId: z.string().uuid().describe("Message UUID"),
173
178
  emoji: z.string().min(1).max(16).describe("One allowed Team Chat emoji"),
174
179
  claimToken: z.string().regex(/^[1-9][0-9]{0,8}:[A-Za-z0-9:_.-]{8,128}$/).optional().describe("Paired AI agent keys only: the turn prompt's reply claim token <generation>:<listener id>; the reaction then ends that turn"),
175
- sourceMessageId: z.string().uuid().optional().describe("With claimToken only: the turn's message id, when the reaction targets ANOTHER message of its thread (default: the reacted message)"),
176
- }, async ({ channelId, messageId, emoji, claimToken, sourceMessageId }) => {
177
- if (sourceMessageId !== undefined && claimToken === undefined)
178
- throw new Error("sourceMessageId needs claimToken (it names the turn the reaction ends); nothing was sent");
179
- const turn = claimToken ? { claimToken, ...(sourceMessageId && sourceMessageId.toLowerCase() !== messageId.toLowerCase() ? { sourceMessageId } : {}) } : {};
180
+ sourceMessageId: z.string().uuid().optional().describe("With claimToken (or turnToken): the turn's message id, when the reaction targets ANOTHER message of its thread (default: the reacted message)"),
181
+ turnToken: z.string().regex(/^[1-9][0-9]{9,15}\.[0-9a-f]{64}$/).optional().describe("(#1213 r3) an AI teammate answering a Grok Bot turn: the turn's turnToken from its payload; REQUIRED for that teammate's reaction"),
182
+ }, async ({ channelId, messageId, emoji, claimToken, sourceMessageId, turnToken }) => {
183
+ if (sourceMessageId !== undefined && claimToken === undefined && turnToken === undefined)
184
+ throw new Error("sourceMessageId needs claimToken or turnToken (it names the turn the reaction belongs to); nothing was sent");
185
+ const source = sourceMessageId && sourceMessageId.toLowerCase() !== messageId.toLowerCase() ? { sourceMessageId } : {};
186
+ const turn = claimToken ? { claimToken, ...source } : turnToken ? { turnToken, ...source } : {};
180
187
  const result = await client.post(`/api/v1/chat/channels/${channelId}/messages/${messageId}/reactions`, { emoji, ...turn });
181
188
  return formatResult(result);
182
189
  }));
183
- strictInput(server.tool("conduyt_stay_silent", "(#802, app #655 c; paired AI agent keys) The turn prompt's stay_silent: END an AI teammate's Team Chat turn without replying or reacting, when no response is warranted (POST /api/v1/chat/agents/me/inbox/claim with action silent, for the turn's message, with the generation and listener id read from claimToken). Completes the claim with outcome silent and moves the agent's inbox cursor as an acknowledgement does; repeating it on the claim it completed answers the same ({ silent: true, outcome, cursor }). Exactly one of a reply, a reaction or a silence ends a turn: a turn that already ended with a reply or a reaction is refused 409 turn_finished (outcome says which), a stale token 409 agent_claim_lost; nothing changes on either, do not retry. Any other key is refused.", {
190
+ strictInput(server.tool("conduyt_stay_silent", "(#802, app #655 c; paired AI agent keys) The turn prompt's stay_silent: END an AI teammate's Team Chat turn without replying or reacting, when no response is warranted (POST /api/v1/chat/agents/me/inbox/claim with action silent, for the turn's message, with the generation and listener id read from claimToken). Completes the claim with outcome silent and moves the agent's inbox cursor as an acknowledgement does; repeating it on the claim it completed answers the same ({ silent: true, outcome, cursor }). Exactly one of a reply, a reaction or a silence ends a turn: a turn that already ended with a reply or a reaction is refused 409 turn_finished (outcome says which; (#1213) also paused or trigger_deleted when the server ended it), a stale token 409 agent_claim_lost; nothing changes on either, do not retry. (#1213) Silence is the exception: use it only when the message is clearly not for this teammate; a turn whose teammate already posted in the channel since it started ends replied, never silent. Any other key is refused.", {
184
191
  messageId: z.string().uuid().describe("The turn's message id (the turn prompt's Message id)"),
185
192
  claimToken: z.string().regex(/^[1-9][0-9]{0,8}:[A-Za-z0-9:_.-]{8,128}$/).describe("The turn prompt's reply claim token <generation>:<listener id>"),
186
193
  }, async ({ messageId, claimToken }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt-mcp",
3
- "version": "4.105.6",
3
+ "version": "4.105.8",
4
4
  "mcpName": "com.conduyt/mcp",
5
5
  "description": "MCP server for Conduyt CRM — expose CRM operations as AI-accessible tools",
6
6
  "type": "module",
@@ -153,7 +153,7 @@
153
153
  {
154
154
  "match": "/api/v1/chat/agents",
155
155
  "status": "excluded",
156
- "reason": "(#599) session-only (assertSessionOnly): an admin manages AI teammates in Settings > AI; every API key is refused"
156
+ "reason": "(#599) creating, removing and pairing an AI teammate are session-only (assertSessionOnly): an admin manages them in Settings > AI; every API key is refused. (#1153) GET /chat/agents is covered by conduyt_list_ai_teammates and the Grok Bot connect routes by their five tools (a covered endpoint is never matched here)"
157
157
  },
158
158
  {
159
159
  "match": "/api/v1/chat",
@@ -1,11 +1,11 @@
1
1
  {
2
- "generatedAt": "2026-11-06T18:54:56.539Z",
3
- "toolCount": 392,
4
- "totalEndpoints": 1160,
2
+ "generatedAt": "2026-11-08T09:31:39.733Z",
3
+ "toolCount": 393,
4
+ "totalEndpoints": 1168,
5
5
  "counts": {
6
- "covered": 346,
7
- "excluded": 356,
8
- "planned": 458,
6
+ "covered": 347,
7
+ "excluded": 360,
8
+ "planned": 461,
9
9
  "uncategorized": 0
10
10
  },
11
11
  "matrix": {
@@ -365,10 +365,26 @@
365
365
  "status": "excluded",
366
366
  "reason": "super-admin platform operations (Conduyt staff only)"
367
367
  },
368
+ "POST /api/v1/admin/external-deletions/:id/confirm-mailbox": {
369
+ "status": "excluded",
370
+ "reason": "super-admin platform operations (Conduyt staff only)"
371
+ },
372
+ "POST /api/v1/admin/external-deletions/:id/release": {
373
+ "status": "excluded",
374
+ "reason": "super-admin platform operations (Conduyt staff only)"
375
+ },
368
376
  "POST /api/v1/admin/external-deletions/:id/replay": {
369
377
  "status": "excluded",
370
378
  "reason": "super-admin platform operations (Conduyt staff only)"
371
379
  },
380
+ "POST /api/v1/admin/external-deletions/:id/resolve": {
381
+ "status": "excluded",
382
+ "reason": "super-admin platform operations (Conduyt staff only)"
383
+ },
384
+ "GET /api/v1/admin/external-deletions/recovery": {
385
+ "status": "excluded",
386
+ "reason": "super-admin platform operations (Conduyt staff only)"
387
+ },
372
388
  "POST /api/v1/admin/impersonate": {
373
389
  "status": "excluded",
374
390
  "reason": "super-admin platform operations (Conduyt staff only)"
@@ -1293,6 +1309,10 @@
1293
1309
  "status": "planned",
1294
1310
  "reason": "call subroutes beyond list/log"
1295
1311
  },
1312
+ "POST /api/v1/calls/:id/diagnostics": {
1313
+ "status": "planned",
1314
+ "reason": "call subroutes beyond list/log"
1315
+ },
1296
1316
  "POST /api/v1/calls/:id/link": {
1297
1317
  "status": "covered",
1298
1318
  "tools": [
@@ -1312,20 +1332,22 @@
1312
1332
  ]
1313
1333
  },
1314
1334
  "GET /api/v1/chat/agents": {
1315
- "status": "excluded",
1316
- "reason": "(#599) session-only (assertSessionOnly): an admin manages AI teammates in Settings > AI; every API key is refused"
1335
+ "status": "covered",
1336
+ "tools": [
1337
+ "conduyt_list_ai_teammates"
1338
+ ]
1317
1339
  },
1318
1340
  "POST /api/v1/chat/agents": {
1319
1341
  "status": "excluded",
1320
- "reason": "(#599) session-only (assertSessionOnly): an admin manages AI teammates in Settings > AI; every API key is refused"
1342
+ "reason": "(#599) creating, removing and pairing an AI teammate are session-only (assertSessionOnly): an admin manages them in Settings > AI; every API key is refused. (#1153) GET /chat/agents is covered by conduyt_list_ai_teammates and the Grok Bot connect routes by their five tools (a covered endpoint is never matched here)"
1321
1343
  },
1322
1344
  "PATCH /api/v1/chat/agents/:id": {
1323
1345
  "status": "excluded",
1324
- "reason": "(#599) session-only (assertSessionOnly): an admin manages AI teammates in Settings > AI; every API key is refused"
1346
+ "reason": "(#599) creating, removing and pairing an AI teammate are session-only (assertSessionOnly): an admin manages them in Settings > AI; every API key is refused. (#1153) GET /chat/agents is covered by conduyt_list_ai_teammates and the Grok Bot connect routes by their five tools (a covered endpoint is never matched here)"
1325
1347
  },
1326
1348
  "DELETE /api/v1/chat/agents/:id": {
1327
1349
  "status": "excluded",
1328
- "reason": "(#599) session-only (assertSessionOnly): an admin manages AI teammates in Settings > AI; every API key is refused"
1350
+ "reason": "(#599) creating, removing and pairing an AI teammate are session-only (assertSessionOnly): an admin manages them in Settings > AI; every API key is refused. (#1153) GET /chat/agents is covered by conduyt_list_ai_teammates and the Grok Bot connect routes by their five tools (a covered endpoint is never matched here)"
1329
1351
  },
1330
1352
  "POST /api/v1/chat/agents/:id/connect": {
1331
1353
  "status": "covered",
@@ -1335,7 +1357,7 @@
1335
1357
  },
1336
1358
  "POST /api/v1/chat/agents/:id/pair-code": {
1337
1359
  "status": "excluded",
1338
- "reason": "(#599) session-only (assertSessionOnly): an admin manages AI teammates in Settings > AI; every API key is refused"
1360
+ "reason": "(#599) creating, removing and pairing an AI teammate are session-only (assertSessionOnly): an admin manages them in Settings > AI; every API key is refused. (#1153) GET /chat/agents is covered by conduyt_list_ai_teammates and the Grok Bot connect routes by their five tools (a covered endpoint is never matched here)"
1339
1361
  },
1340
1362
  "GET /api/v1/chat/agents/:id/webhook": {
1341
1363
  "status": "covered",
@@ -1379,6 +1401,10 @@
1379
1401
  "conduyt_stay_silent"
1380
1402
  ]
1381
1403
  },
1404
+ "PUT /api/v1/chat/agents/me/knowledge": {
1405
+ "status": "excluded",
1406
+ "reason": "(#599) the listener protocol of a paired agent key (long poll, per-event ack, heartbeat): served by the npm CLI `conduyt agent listen`, not a request/response MCP tool"
1407
+ },
1382
1408
  "GET /api/v1/chat/agents/me/mcp": {
1383
1409
  "status": "excluded",
1384
1410
  "reason": "(#599) the listener protocol of a paired agent key (long poll, per-event ack, heartbeat): served by the npm CLI `conduyt agent listen`, not a request/response MCP tool"
@@ -1467,6 +1493,14 @@
1467
1493
  "conduyt_list_chat_thread_files"
1468
1494
  ]
1469
1495
  },
1496
+ "PUT /api/v1/chat/channels/:id/messages/:messageId/follow": {
1497
+ "status": "planned",
1498
+ "reason": "team chat read/post for agents"
1499
+ },
1500
+ "DELETE /api/v1/chat/channels/:id/messages/:messageId/follow": {
1501
+ "status": "planned",
1502
+ "reason": "team chat read/post for agents"
1503
+ },
1470
1504
  "POST /api/v1/chat/channels/:id/messages/:messageId/handoff": {
1471
1505
  "status": "planned",
1472
1506
  "reason": "team chat read/post for agents"