conduyt-mcp 4.105.5 → 4.105.7

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
- ## 387 Tools
5
+ ## 393 Tools
6
6
 
7
7
  ### Discovery
8
8
  | Tool | Description |
@@ -270,7 +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
+
275
+ ### A Grok Bot as an AI teammate (#1140)
276
+ | Tool | Description |
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 |
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 |
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) |
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 }` |
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 |
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 |
284
+
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."
274
286
 
275
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.
276
288
 
@@ -26,6 +26,7 @@ import { registerMailchimpTools } from "./tools/mailchimp.js";
26
26
  import { registerStatusMetadataTools } from "./tools/status-metadata.js";
27
27
  import { registerNotificationTools } from "./tools/notifications.js";
28
28
  import { registerChatTools } from "./tools/chat.js";
29
+ import { registerAiTeammateConnectTools } from "./tools/ai-teammate-connect.js";
29
30
  import { registerCustomFieldTools } from "./tools/custom-fields.js";
30
31
  import { registerSmartListTools } from "./tools/smart-lists.js";
31
32
  import { registerUserTools } from "./tools/users.js";
@@ -90,6 +91,7 @@ export function registerAllTools(server, client) {
90
91
  registerStatusMetadataTools(server, client);
91
92
  registerNotificationTools(server, client);
92
93
  registerChatTools(server, client);
94
+ registerAiTeammateConnectTools(server, client);
93
95
  registerCustomFieldTools(server, client);
94
96
  registerSmartListTools(server, client);
95
97
  registerUserTools(server, client);
@@ -190,6 +190,15 @@ export const TOOL_ANNOTATIONS = {
190
190
  conduyt_get_chat_linked_record: ro("Get Chat Linked Record"),
191
191
  // (#684) the thread reads recover a first-use 403 through GET /chat/channels, which creates auto-channel memberships (as conduyt_list_chat_messages)
192
192
  conduyt_get_chat_thread: update("Get Chat Thread"),
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
+ // 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"),
197
+ conduyt_connect_ai_teammate: row("Connect AI Teammate", false, true, false),
198
+ conduyt_set_ai_teammate_webhook: row("Set AI Teammate Webhook", false, false, false),
199
+ conduyt_test_ai_teammate_webhook: create("Test AI Teammate Webhook"),
200
+ conduyt_get_ai_teammate_webhook: ro("Get AI Teammate Webhook"),
201
+ conduyt_disconnect_ai_teammate: destroy("Disconnect AI Teammate"),
193
202
  conduyt_list_chat_thread_files: update("List Chat Thread Files"),
194
203
  conduyt_mark_chat_read: row("Mark Chat Read", false, false, false), // every call re-announces the read receipt to the reader's other tabs and devices, even when the marker did not move (chat/channels/[id]/messages/route.ts:126)
195
204
  conduyt_get_chat_realtime_token: ro("Get Chat Realtime Token"),
@@ -0,0 +1,28 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { ConduytApiError, ConduytClient } from "../client.js";
3
+ export { hide } from "./routine-key-redaction.js";
4
+ /**
5
+ * (#1140 lane A3) Connecting a Grok Bot routine as an AI teammate, the tri-surface twin of the Settings > AI card and of the CLI's
6
+ * `agent connect` / `agent webhook set|test|status` / `agent disconnect`. Built against conduyt-app lane A2b at 0c0088508
7
+ * (app/api/v1/chat/agents/[id]/connect, .../webhook, .../webhook/test; every sentence lives in lib/agent-webhook-connect.ts) and the
8
+ * concept page Cerebro domains/conduyt/wiki/concepts/grok-bot-teammate-webhook-and-remote-tools-2026-11-05.md (section S).
9
+ *
10
+ * The routine's key is WRITE-ONLY: it goes into the PUT body and nowhere else. The server never answers it, and every text these tools
11
+ * return (results and refusals) is passed through `hide`, which replaces it wherever it appears. Every refusal is an error result with
12
+ * ONE plain sentence: the server's own (the S.3 and input sentences are written for people) or ours where its text is not one (today's
13
+ * production answers these paths with an HTML 404; the switch answers "Not found").
14
+ */
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
+ 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 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.";
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).";
21
+ export declare const NO_CREDENTIALS_SENTENCE = "Give the routine's address and key: paste (the block Grok Bot shows), or address and key.";
22
+ /** the one sentence for a refused call, and its code when the API sent one */
23
+ export declare function refusalOf(err: ConduytApiError): {
24
+ status: number | null;
25
+ code: string | null;
26
+ sentence: string;
27
+ };
28
+ export declare function registerAiTeammateConnectTools(server: McpServer, client: ConduytClient): void;
@@ -0,0 +1,139 @@
1
+ import { z } from "zod";
2
+ import { ConduytApiError, formatToolError } from "../client.js";
3
+ import { strictInput } from "./strict.js";
4
+ import { hide, secretsInPaste, secretsOfKey } from "./routine-key-redaction.js";
5
+ export { hide } from "./routine-key-redaction.js";
6
+ /**
7
+ * (#1140 lane A3) Connecting a Grok Bot routine as an AI teammate, the tri-surface twin of the Settings > AI card and of the CLI's
8
+ * `agent connect` / `agent webhook set|test|status` / `agent disconnect`. Built against conduyt-app lane A2b at 0c0088508
9
+ * (app/api/v1/chat/agents/[id]/connect, .../webhook, .../webhook/test; every sentence lives in lib/agent-webhook-connect.ts) and the
10
+ * concept page Cerebro domains/conduyt/wiki/concepts/grok-bot-teammate-webhook-and-remote-tools-2026-11-05.md (section S).
11
+ *
12
+ * The routine's key is WRITE-ONLY: it goes into the PUT body and nowhere else. The server never answers it, and every text these tools
13
+ * return (results and refusals) is passed through `hide`, which replaces it wherever it appears. Every refusal is an error result with
14
+ * ONE plain sentence: the server's own (the S.3 and input sentences are written for people) or ours where its text is not one (today's
15
+ * production answers these paths with an HTML 404; the switch answers "Not found").
16
+ */
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
+ 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 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.";
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).";
23
+ export const NO_CREDENTIALS_SENTENCE = "Give the routine's address and key: paste (the block Grok Bot shows), or address and key.";
24
+ function errorDetailsOf(message) {
25
+ for (let i = message.indexOf("{"); i !== -1; i = message.indexOf("{", i + 1)) {
26
+ try {
27
+ const v = JSON.parse(message.slice(i));
28
+ if (v && typeof v === "object" && !Array.isArray(v))
29
+ return v;
30
+ }
31
+ catch { /* not the details object */ }
32
+ }
33
+ return {};
34
+ }
35
+ /** the API's own error line (between "Conduyt API <status>: " and its Retry-After tag or details object) */
36
+ function apiErrorText(message) {
37
+ const m = /^Conduyt API \d{3}: ([\s\S]*)$/.exec(message);
38
+ let t = m ? m[1] : message;
39
+ const brace = t.search(/ \[Retry-After: |\s\{/);
40
+ if (brace !== -1)
41
+ t = t.slice(0, brace);
42
+ return t.replace(/\s+/g, " ").trim();
43
+ }
44
+ /** the one sentence for a refused call, and its code when the API sent one */
45
+ export function refusalOf(err) {
46
+ const details = errorDetailsOf(err.message);
47
+ const code = typeof details.code === "string" ? details.code : null;
48
+ const text = apiErrorText(err.message);
49
+ const status = err.status;
50
+ if (code === "ai_teammates_off")
51
+ return { status, code, sentence: AI_OFF_SENTENCE };
52
+ if (code === "agent_route_refused")
53
+ return { status, code, sentence: TEAMMATE_KEY_SENTENCE };
54
+ if (status === 404 && text === "Agent not found")
55
+ return { status, code, sentence: NOT_FOUND_SENTENCE };
56
+ // today's production: its HTML not-found page (404 text/html, before any auth); a 405: a route without this method
57
+ if (status === 404 || status === 405)
58
+ return { status, code, sentence: OLDER_SERVER_SENTENCE };
59
+ return { status, code, sentence: text.slice(0, 600) || `Conduyt answered ${status} without an explanation.` };
60
+ }
61
+ /** a structured, non-retried error result: { ok: false, status, code?, error: <one sentence>, ...extra } */
62
+ function connectError(sentence, secrets, extra = {}) {
63
+ const base = formatToolError(hide(sentence, secrets));
64
+ return { ...base, content: [{ type: "text", text: hide(JSON.stringify({ ok: false, error: sentence, ...extra }, null, 2), secrets) }] };
65
+ }
66
+ /** a result whose text is redacted of the secrets */
67
+ function connectResult(data, secrets) {
68
+ return { content: [{ type: "text", text: hide(JSON.stringify(data, null, 2), secrets) }] };
69
+ }
70
+ /** runs one call: an HTTP refusal becomes the one-sentence error result; any other failure is re-thrown with the secrets hidden */
71
+ async function attempt(secrets, call, done) {
72
+ try {
73
+ return done(await call());
74
+ }
75
+ catch (err) {
76
+ if (err instanceof ConduytApiError && err.code === "http") {
77
+ const r = refusalOf(err);
78
+ return connectError(r.sentence, secrets, { status: r.status, ...(r.code ? { code: r.code } : {}) });
79
+ }
80
+ throw new Error(hide(err instanceof Error ? err.message : String(err), secrets));
81
+ }
82
+ }
83
+ const unwrap = (res) => (res && typeof res === "object" && "data" in res ? res.data : res);
84
+ /** a test's answer: the data, or (the test was not accepted) an error result with the S.3 sentence and the whole answer beside it */
85
+ function testOutcome(secrets) {
86
+ return (res) => {
87
+ const d = unwrap(res);
88
+ if (d?.test?.ok === true)
89
+ return connectResult(d, secrets);
90
+ const problem = d?.test?.problem;
91
+ return connectError(problem?.message ?? "Grok Bot did not accept the test.", secrets, { status: 200, ...(problem?.code ? { code: problem.code } : {}), answer: d });
92
+ };
93
+ }
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());
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.";
104
+ export function registerAiTeammateConnectTools(server, client) {
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, 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), 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), []))));
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}`, {
123
+ teammateId: TEAMMATE_ID,
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"),
125
+ address: z.string().min(1).max(2_048).optional().describe("The routine's address (https://...)"),
126
+ key: z.string().min(1).max(600).optional().describe("The routine's key (write-only: never returned). The bare key or the \"Authorization: Bearer ...\" header value"),
127
+ }, async ({ teammateId, paste, address, key }) => {
128
+ // (r2, Codex grokA3-mcp-r1 P1) every form the server could take as the key (routine-key-redaction.ts: the server's rules ported,
129
+ // plus the floor of every token as typed), for each input that can carry it: the key field, the paste, an address box holding both
130
+ const secrets = [...(key !== undefined ? secretsOfKey(key) : []), ...(paste !== undefined ? secretsInPaste(paste) : []), ...(address !== undefined ? secretsInPaste(address) : [])];
131
+ if (paste === undefined && address === undefined)
132
+ return connectError(NO_CREDENTIALS_SENTENCE, secrets, { status: null, code: "address_missing" });
133
+ // the body as ONE literal: the contract gate reads the request fields (paste, address, key) from it
134
+ return attempt(secrets, () => client.put(`/api/v1/chat/agents/${seg(teammateId)}/webhook`, { ...(paste !== undefined ? { paste } : {}), ...(address !== undefined ? { address } : {}), ...(key !== undefined ? { key } : {}) }), testOutcome(secrets));
135
+ }));
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), []))));
139
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * (#1140 lane A3 r2) What to redact so the Grok Bot routine key never leaves this program: every form the server could take as the
3
+ * key, plus a floor. THIS FILE IS IDENTICAL in conduyt-cli (src/routine-key-redaction.ts) and conduyt-mcp
4
+ * (src/tools/routine-key-redaction.ts), and so is its test (routine-key-redaction.test.ts beside it): the two repos cannot import from
5
+ * each other or from the app, so the same table runs against each copy and a drift fails a test. Findings it answers: Codex
6
+ * grokA3-cli-r1 P2 2 and grokA3-mcp-r1 P1.
7
+ */
8
+ /** replaces every appearance of each secret (4 characters or more) in a text about to be printed or returned */
9
+ export declare function hide(text: string, secrets: readonly string[]): string;
10
+ /** :157-163 normalizeRoutineKey (the key field, and every candidate of a block at :191) */
11
+ export declare function serverNormalizeKey(raw: string): string | null;
12
+ /** :176-198 parseRoutineBlock's key extraction: the text without its addresses (:184), then crsr_ tokens (:186), the token after
13
+ * "Bearer " (:187), the token after a line-leading "key:" / "key =" (:188), each with trailing punctuation cut and normalized (:191).
14
+ * Returns EVERY normalized candidate (the server takes exactly one; all of them are hidden). */
15
+ export declare function serverKeyCandidates(text: string): string[];
16
+ /** the FLOOR: every whitespace-separated token of the input as typed (not an address) of 8 characters or more, in each shape a strip
17
+ * of surrounding quotes or brackets, a leading label (key: / key= / Authorization: / Bearer) or trailing punctuation gives it */
18
+ export declare function rawKeyTokens(text: string): string[];
19
+ /** what to hide for a pasted block (the server's candidates, then the floor) */
20
+ export declare function secretsInPaste(text: string): string[];
21
+ /** what to hide for a key given on its own (:213 normalizes the key field; :210 reads an address-alone body as a block) */
22
+ export declare function secretsOfKey(raw: string): string[];
@@ -0,0 +1,88 @@
1
+ /**
2
+ * (#1140 lane A3 r2) What to redact so the Grok Bot routine key never leaves this program: every form the server could take as the
3
+ * key, plus a floor. THIS FILE IS IDENTICAL in conduyt-cli (src/routine-key-redaction.ts) and conduyt-mcp
4
+ * (src/tools/routine-key-redaction.ts), and so is its test (routine-key-redaction.test.ts beside it): the two repos cannot import from
5
+ * each other or from the app, so the same table runs against each copy and a drift fails a test. Findings it answers: Codex
6
+ * grokA3-cli-r1 P2 2 and grokA3-mcp-r1 P1.
7
+ */
8
+ /** replaces every appearance of each secret (4 characters or more) in a text about to be printed or returned */
9
+ export function hide(text, secrets) {
10
+ let out = text;
11
+ // (r2) longest first: a shorter secret inside a longer one never leaves the longer one's tail behind
12
+ for (const s of [...secrets].sort((a, b) => b.length - a.length))
13
+ if (s && s.length >= 4)
14
+ out = out.split(s).join("[hidden]");
15
+ return out;
16
+ }
17
+ // A PORT of the server's extraction, conduyt-app lane A2b 0c0088508 lib/agent-webhook-connect.ts, rule for rule and in its order.
18
+ // It is used ONLY to know what to hide, never to decide what is sent (the server parses the paste itself). Where the port and the
19
+ // server could ever disagree, the floor (rawKeyTokens) still hides the key: every token of the input as typed, in every shape a
20
+ // strip of quotes, labels and trailing punctuation can give it.
21
+ /** lib/agent-webhook-connect.ts:146 KEY_RE */
22
+ const SERVER_KEY_RE = /^[A-Za-z0-9._~+/=-]{8,512}$/;
23
+ /** :147 URL_IN_TEXT_RE */
24
+ const SERVER_URL_IN_TEXT_RE = /https?:\/\/[^\s"'`<>]+/gi;
25
+ /** :148 TRAILING_PUNCT_RE */
26
+ const SERVER_TRAILING_PUNCT_RE = /[.,;:!?)\]}]+$/;
27
+ /** :150-154 unquote */
28
+ function serverUnquote(raw) {
29
+ let out = raw.trim();
30
+ while (out.length >= 2 && /^["'`<]/.test(out) && /["'`>]$/.test(out))
31
+ out = out.slice(1, -1).trim();
32
+ return out;
33
+ }
34
+ /** :157-163 normalizeRoutineKey (the key field, and every candidate of a block at :191) */
35
+ export function serverNormalizeKey(raw) {
36
+ let s = serverUnquote(raw);
37
+ s = s.replace(/^authorization\s*:\s*/i, "");
38
+ s = s.replace(/^bearer\s+/i, "");
39
+ s = serverUnquote(s);
40
+ return SERVER_KEY_RE.test(s) ? s : null;
41
+ }
42
+ /** :176-198 parseRoutineBlock's key extraction: the text without its addresses (:184), then crsr_ tokens (:186), the token after
43
+ * "Bearer " (:187), the token after a line-leading "key:" / "key =" (:188), each with trailing punctuation cut and normalized (:191).
44
+ * Returns EVERY normalized candidate (the server takes exactly one; all of them are hidden). */
45
+ export function serverKeyCandidates(text) {
46
+ const rest = text.replace(SERVER_URL_IN_TEXT_RE, " ");
47
+ const found = [];
48
+ for (const m of rest.matchAll(/(?:^|[^A-Za-z0-9_])(crsr_[A-Za-z0-9_-]+)/g))
49
+ found.push(m[1]);
50
+ for (const m of rest.matchAll(/\bbearer\s+([^\s"'`]+)/gi))
51
+ found.push(m[1]);
52
+ for (const m of rest.matchAll(/^[ \t]*(?:the\s+)?key\s*[:=][ \t]*["'`]?([^\s"'`]+)/gim))
53
+ found.push(m[1]);
54
+ const out = [];
55
+ for (const c of found) {
56
+ const k = serverNormalizeKey(c.replace(SERVER_TRAILING_PUNCT_RE, ""));
57
+ if (k)
58
+ out.push(k);
59
+ }
60
+ return out;
61
+ }
62
+ /** the FLOOR: every whitespace-separated token of the input as typed (not an address) of 8 characters or more, in each shape a strip
63
+ * of surrounding quotes or brackets, a leading label (key: / key= / Authorization: / Bearer) or trailing punctuation gives it */
64
+ export function rawKeyTokens(text) {
65
+ const out = new Set();
66
+ const add = (t) => { if (t.length >= 8 && !/^https?:\/\//i.test(t))
67
+ out.add(t); };
68
+ for (const tok of text.split(/\s+/)) {
69
+ const shapes = [tok];
70
+ for (let i = 0; i < 3; i++) {
71
+ for (const t of [...shapes]) {
72
+ shapes.push(t.replace(/^["'`<(\[{]+|["'`>)\]}]+$/g, ""), t.replace(SERVER_TRAILING_PUNCT_RE, ""), t.replace(/^(?:key\s*[:=]|authorization\s*:|bearer\s*:?)/i, ""));
73
+ }
74
+ }
75
+ for (const t of shapes)
76
+ add(t);
77
+ }
78
+ return [...out];
79
+ }
80
+ /** what to hide for a pasted block (the server's candidates, then the floor) */
81
+ export function secretsInPaste(text) {
82
+ return [...new Set([...serverKeyCandidates(text), ...rawKeyTokens(text)])];
83
+ }
84
+ /** what to hide for a key given on its own (:213 normalizes the key field; :210 reads an address-alone body as a block) */
85
+ export function secretsOfKey(raw) {
86
+ const n = serverNormalizeKey(raw);
87
+ return [...new Set([raw, raw.trim(), ...(n ? [n] : []), ...rawKeyTokens(raw)])].filter((s) => s.length >= 4);
88
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt-mcp",
3
- "version": "4.105.5",
3
+ "version": "4.105.7",
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-06T12:12:53.647Z",
3
- "toolCount": 387,
4
- "totalEndpoints": 1152,
2
+ "generatedAt": "2026-11-06T22:13:20.826Z",
3
+ "toolCount": 393,
4
+ "totalEndpoints": 1162,
5
5
  "counts": {
6
- "covered": 341,
7
- "excluded": 353,
8
- "planned": 458,
6
+ "covered": 347,
7
+ "excluded": 355,
8
+ "planned": 460,
9
9
  "uncategorized": 0
10
10
  },
11
11
  "matrix": {
@@ -1312,24 +1312,56 @@
1312
1312
  ]
1313
1313
  },
1314
1314
  "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"
1315
+ "status": "covered",
1316
+ "tools": [
1317
+ "conduyt_list_ai_teammates"
1318
+ ]
1317
1319
  },
1318
1320
  "POST /api/v1/chat/agents": {
1319
1321
  "status": "excluded",
1320
- "reason": "(#599) session-only (assertSessionOnly): an admin manages AI teammates in Settings > AI; every API key is refused"
1322
+ "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
1323
  },
1322
1324
  "PATCH /api/v1/chat/agents/:id": {
1323
1325
  "status": "excluded",
1324
- "reason": "(#599) session-only (assertSessionOnly): an admin manages AI teammates in Settings > AI; every API key is refused"
1326
+ "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
1327
  },
1326
1328
  "DELETE /api/v1/chat/agents/:id": {
1327
1329
  "status": "excluded",
1328
- "reason": "(#599) session-only (assertSessionOnly): an admin manages AI teammates in Settings > AI; every API key is refused"
1330
+ "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)"
1331
+ },
1332
+ "POST /api/v1/chat/agents/:id/connect": {
1333
+ "status": "covered",
1334
+ "tools": [
1335
+ "conduyt_connect_ai_teammate"
1336
+ ]
1329
1337
  },
1330
1338
  "POST /api/v1/chat/agents/:id/pair-code": {
1331
1339
  "status": "excluded",
1332
- "reason": "(#599) session-only (assertSessionOnly): an admin manages AI teammates in Settings > AI; every API key is refused"
1340
+ "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)"
1341
+ },
1342
+ "GET /api/v1/chat/agents/:id/webhook": {
1343
+ "status": "covered",
1344
+ "tools": [
1345
+ "conduyt_get_ai_teammate_webhook"
1346
+ ]
1347
+ },
1348
+ "PUT /api/v1/chat/agents/:id/webhook": {
1349
+ "status": "covered",
1350
+ "tools": [
1351
+ "conduyt_set_ai_teammate_webhook"
1352
+ ]
1353
+ },
1354
+ "DELETE /api/v1/chat/agents/:id/webhook": {
1355
+ "status": "covered",
1356
+ "tools": [
1357
+ "conduyt_disconnect_ai_teammate"
1358
+ ]
1359
+ },
1360
+ "POST /api/v1/chat/agents/:id/webhook/test": {
1361
+ "status": "covered",
1362
+ "tools": [
1363
+ "conduyt_test_ai_teammate_webhook"
1364
+ ]
1333
1365
  },
1334
1366
  "POST /api/v1/chat/agents/me/heartbeat": {
1335
1367
  "status": "excluded",
@@ -1349,6 +1381,18 @@
1349
1381
  "conduyt_stay_silent"
1350
1382
  ]
1351
1383
  },
1384
+ "GET /api/v1/chat/agents/me/mcp": {
1385
+ "status": "excluded",
1386
+ "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"
1387
+ },
1388
+ "POST /api/v1/chat/agents/me/mcp": {
1389
+ "status": "excluded",
1390
+ "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"
1391
+ },
1392
+ "DELETE /api/v1/chat/agents/me/mcp": {
1393
+ "status": "excluded",
1394
+ "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"
1395
+ },
1352
1396
  "POST /api/v1/chat/agents/pair": {
1353
1397
  "status": "excluded",
1354
1398
  "reason": "(#599) the single-use pair-code exchange that MINTS the agent's key: the listener's first call (npm CLI `conduyt agent pair` / `listen --pair-code`), never made with an existing key"
@@ -1425,6 +1469,14 @@
1425
1469
  "conduyt_list_chat_thread_files"
1426
1470
  ]
1427
1471
  },
1472
+ "PUT /api/v1/chat/channels/:id/messages/:messageId/follow": {
1473
+ "status": "planned",
1474
+ "reason": "team chat read/post for agents"
1475
+ },
1476
+ "DELETE /api/v1/chat/channels/:id/messages/:messageId/follow": {
1477
+ "status": "planned",
1478
+ "reason": "team chat read/post for agents"
1479
+ },
1428
1480
  "POST /api/v1/chat/channels/:id/messages/:messageId/handoff": {
1429
1481
  "status": "planned",
1430
1482
  "reason": "team chat read/post for agents"