conduyt-mcp 4.105.3 → 4.105.4

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
- ## 383 Tools
5
+ ## 385 Tools
6
6
 
7
7
  ### Discovery
8
8
  | Tool | Description |
@@ -255,7 +255,9 @@ The Mailchimp OAuth connect and callback are browser-only: connect Mailchimp in
255
255
  | `conduyt_list_chat_messages` | List a channel's messages, newest first; every message carries `seq` (commit-ordered position in the channel); does not move the read marker; on a 403 "Not a member of this channel" it calls `GET /chat/channels` once and retries once |
256
256
  | `conduyt_send_chat_message` | Send a message through the canonical audited Team Chat writer; agent keys receive the six-turn human-handoff guard |
257
257
  | `conduyt_reply_chat_message` | Reply to a top-level message in its thread through the same audited writer; an agent passes `idempotencyKey` (`reply:<source message id>`) so a retried turn returns the first reply (200, `idempotentReplay: true`) instead of posting twice |
258
- | `conduyt_react_chat_message` | React to a Team Chat message as the authenticated user/agent |
258
+ | `conduyt_react_chat_message` | React to a Team Chat message as the authenticated user/agent; an AI teammate passes `claimToken` (the turn prompt's reply claim token, plus `sourceMessageId` when it reacts to another message of the thread) and the reaction ENDS that turn (outcome reacted); 409 `reaction_stale_claim` / `turn_finished` write nothing |
259
+ | `conduyt_stay_silent` | (#802) the turn prompt's stay_silent: ends an AI teammate's turn with no response (`POST /chat/agents/me/inbox/claim` action `silent` for the turn's `messageId`, generation and listener id read from `claimToken`); 409 `turn_finished` when it already replied or reacted |
260
+ | `conduyt_get_chat_linked_record` | (#802) the turn prompt's linked_record: `GET /chat/channels/{id}/linked-record`, the channel's linked deal or contact as a read-only summary `{ kind, record }` under the caller's visibility; one 404 for not a member / no link / not visible |
259
261
  | `conduyt_mark_chat_read` | Acknowledge read up to one message (`readUpTo` = message UUID; the marker only moves forward, an older or foreign message is a no-op; same one-retry membership bootstrap) |
260
262
  | `conduyt_get_chat_realtime_token` | `GET /realtime/token`: a signed Ably token request for the realtime pipe (`channelIds` up to 10, sent as repeated `channel=`); `{ enabled: false, reason }` when the account switch is off; returns `accountId` / `userId` |
261
263
  | `conduyt_get_chat_notifications` | `GET /chat/channels/{id}/notifications`: your level `{ level, isDefault, notify, updatedAt, membershipId, membershipSince, membershipSeq }` |
@@ -264,6 +266,8 @@ The Mailchimp OAuth connect and callback are browser-only: connect Mailchimp in
264
266
 
265
267
  `conduyt_list_chat_messages` also takes `afterSeq` (messages committed after that seq; once, a whole number, never with `cursor`) and every message carries `version` (a counter bumped on each update). `conduyt_get_chat_unread` rows carry `mentions`, `notify`, `updatedAt`, `membershipId` (null = no membership), `membershipSince`, `membershipSeq`, and the response `teamChatRealtime` and `membershipSeqHigh`; `conduyt_list_chat_channels` rows carry `mentionCount` and the same membership fields.
266
268
 
269
+ 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.
270
+
267
271
  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.
268
272
 
269
273
  #### What the API publishes (realtime events, not tools)
@@ -185,6 +185,9 @@ export const TOOL_ANNOTATIONS = {
185
185
  conduyt_send_chat_message: create("Send Chat Message"),
186
186
  conduyt_reply_chat_message: create("Reply to Chat Message"),
187
187
  conduyt_react_chat_message: update("React to Chat Message"),
188
+ // (#802) ends an AI teammate's turn with outcome silent; a repeat on the claim it completed answers the same (like an acknowledgement)
189
+ conduyt_stay_silent: update("Stay Silent on a Chat Turn"),
190
+ conduyt_get_chat_linked_record: ro("Get Chat Linked Record"),
188
191
  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)
189
192
  conduyt_get_chat_realtime_token: ro("Get Chat Realtime Token"),
190
193
  conduyt_get_chat_notifications: update("Get Chat Notifications"),
@@ -91,12 +91,29 @@ export function registerChatTools(server, client) {
91
91
  const result = await client.post(`/api/v1/chat/channels/${channelId}/messages`, { body, parentId, ...(idempotencyKey ? { idempotencyKey } : {}), ...(claimToken ? { claimToken } : {}) });
92
92
  return formatResult(result);
93
93
  }));
94
- 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.", {
94
+ 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.", {
95
95
  channelId: z.string().uuid().describe("Channel UUID"),
96
96
  messageId: z.string().uuid().describe("Message UUID"),
97
97
  emoji: z.string().min(1).max(16).describe("One allowed Team Chat emoji"),
98
- }, async ({ channelId, messageId, emoji }) => {
99
- const result = await client.post(`/api/v1/chat/channels/${channelId}/messages/${messageId}/reactions`, { emoji });
98
+ 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"),
99
+ 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)"),
100
+ }, async ({ channelId, messageId, emoji, claimToken, sourceMessageId }) => {
101
+ if (sourceMessageId !== undefined && claimToken === undefined)
102
+ throw new Error("sourceMessageId needs claimToken (it names the turn the reaction ends); nothing was sent");
103
+ const turn = claimToken ? { claimToken, ...(sourceMessageId && sourceMessageId.toLowerCase() !== messageId.toLowerCase() ? { sourceMessageId } : {}) } : {};
104
+ const result = await client.post(`/api/v1/chat/channels/${channelId}/messages/${messageId}/reactions`, { emoji, ...turn });
105
+ return formatResult(result);
106
+ }));
107
+ 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.", {
108
+ messageId: z.string().uuid().describe("The turn's message id (the turn prompt's Message id)"),
109
+ 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>"),
110
+ }, async ({ messageId, claimToken }) => {
111
+ const m = /^([1-9][0-9]{0,8}):([A-Za-z0-9:_.-]{8,128})$/.exec(claimToken);
112
+ const result = await client.post("/api/v1/chat/agents/me/inbox/claim", { action: "silent", messageId, listenerId: m[2], generation: Number(m[1]) });
113
+ return formatResult(result);
114
+ }));
115
+ strictInput(server.tool("conduyt_get_chat_linked_record", "(#802, app #655 b) The turn prompt's linked_record: the deal or contact a Team Chat channel is linked to, as a bounded READ-ONLY summary (GET /api/v1/chat/channels/{id}/linked-record): { kind: 'deal', record: { id, title, stage { id, name }, pipeline { id, name }, value, status, owner, contact (null when not visible), updatedAt } } or { kind: 'contact', record: { id, firstName, lastName, phone, email, company, owner, tags, updatedAt, openDeals } }, under the caller's own visibility (an AI teammate's key reads as its owner sees it). For a member of the channel; not a member, no such channel, no link, or a record the caller may not read is one identical 404, so the answer never reveals that a fenced record exists. It is the one read of a linked record an AI teammate's key has (chat scope; no contacts scope is added to the key). An AI teammate's turn prompt says when the channel is linked (\"This channel is linked to deal <id>\").", { channelId: z.string().uuid().describe("Channel UUID") }, async ({ channelId }) => {
116
+ const result = await client.get(`/api/v1/chat/channels/${channelId}/linked-record`);
100
117
  return formatResult(result);
101
118
  }));
102
119
  strictInput(server.tool("conduyt_mark_chat_read", "Acknowledge team chat as read up to one message (GET /api/v1/chat/channels/{id}/messages?readUpTo=<messageId>&limit=1; the API takes the acknowledgement as a query on the message list). readUpTo is the UUID of a message you were served (its seq becomes your read marker); take it from conduyt_list_chat_messages. The marker only moves FORWARD, never to 'now': an older (stale) message is a no-op, and a message that is not in this channel moves nothing (the call still succeeds, so re-check conduyt_get_chat_unread). A malformed id is a 400. On a 403 'Not a member of this channel' it calls GET /chat/channels once (creating the caller's auto-channel memberships) and retries ONCE; a second refusal, or a 403 for any other reason, is returned as is. The response is the newest message of the channel (limit 1), not a confirmation of the move.", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt-mcp",
3
- "version": "4.105.3",
3
+ "version": "4.105.4",
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",
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "generatedAt": "2026-11-05T14:27:30.253Z",
3
- "toolCount": 383,
3
+ "toolCount": 385,
4
4
  "totalEndpoints": 1150,
5
5
  "counts": {
6
- "covered": 337,
7
- "excluded": 354,
8
- "planned": 459,
6
+ "covered": 339,
7
+ "excluded": 353,
8
+ "planned": 458,
9
9
  "uncategorized": 0
10
10
  },
11
11
  "matrix": {
@@ -1344,8 +1344,10 @@
1344
1344
  "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"
1345
1345
  },
1346
1346
  "POST /api/v1/chat/agents/me/inbox/claim": {
1347
- "status": "excluded",
1348
- "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"
1347
+ "status": "covered",
1348
+ "tools": [
1349
+ "conduyt_stay_silent"
1350
+ ]
1349
1351
  },
1350
1352
  "POST /api/v1/chat/agents/pair": {
1351
1353
  "status": "excluded",
@@ -1374,8 +1376,10 @@
1374
1376
  "reason": "(#599) session-only (assertSessionOnly): a channel's agents-respond mode is an admin setting; every API key is refused"
1375
1377
  },
1376
1378
  "GET /api/v1/chat/channels/:id/linked-record": {
1377
- "status": "planned",
1378
- "reason": "team chat read/post for agents"
1379
+ "status": "covered",
1380
+ "tools": [
1381
+ "conduyt_get_chat_linked_record"
1382
+ ]
1379
1383
  },
1380
1384
  "POST /api/v1/chat/channels/:id/members": {
1381
1385
  "status": "planned",