conduyt-mcp 4.103.0 → 4.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
- ## 373 Tools
5
+ ## 376 Tools
6
6
 
7
7
  ### Discovery
8
8
  | Tool | Description |
@@ -244,6 +244,9 @@ The Mailchimp OAuth connect and callback are browser-only: connect Mailchimp in
244
244
  | `conduyt_list_chat_channels` | `GET /chat/channels`: your channels; also CREATES your memberships in the auto channels the first time |
245
245
  | `conduyt_get_chat_unread` | `GET /chat/unread`: per channel `{ channelId, unread, soundUnread, newestSeq, latestSeq }`; may list an auto channel you have not opened yet (no membership row) |
246
246
  | `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 |
247
+ | `conduyt_send_chat_message` | Send a message through the canonical audited Team Chat writer; agent keys receive the six-turn human-handoff guard |
248
+ | `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 |
249
+ | `conduyt_react_chat_message` | React to a Team Chat message as the authenticated user/agent |
247
250
  | `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) |
248
251
  | `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` |
249
252
  | `conduyt_get_chat_notifications` | `GET /chat/channels/{id}/notifications`: your level `{ level, isDefault, notify, updatedAt, membershipId, membershipSince, membershipSeq }` |
@@ -252,7 +255,7 @@ The Mailchimp OAuth connect and callback are browser-only: connect Mailchimp in
252
255
 
253
256
  `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.
254
257
 
255
- The rest of team chat (sending, edit/delete, reactions, pins, claim/handoff, typing, presence, uploads) is not wrapped yet. `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.
258
+ 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.
256
259
 
257
260
  #### What the API publishes (realtime events, not tools)
258
261
 
@@ -174,6 +174,9 @@ export const TOOL_ANNOTATIONS = {
174
174
  conduyt_list_chat_channels: update("List Chat Channels"),
175
175
  conduyt_get_chat_unread: ro("Get Chat Unread Counts"),
176
176
  conduyt_list_chat_messages: update("List Chat Messages"),
177
+ conduyt_send_chat_message: create("Send Chat Message"),
178
+ conduyt_reply_chat_message: create("Reply to Chat Message"),
179
+ conduyt_react_chat_message: update("React to Chat Message"),
177
180
  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)
178
181
  conduyt_get_chat_realtime_token: ro("Get Chat Realtime Token"),
179
182
  conduyt_get_chat_notifications: update("Get Chat Notifications"),
@@ -43,7 +43,7 @@ async function withMembershipBootstrap(client, read) {
43
43
  }
44
44
  /** Team chat read side (app 10-09, live sha 3869c79ba): the unread vector, messages carrying `seq`, and the read acknowledgement. */
45
45
  export function registerChatTools(server, client) {
46
- strictInput(server.tool("conduyt_list_chat_channels", "List the team chat channels the API key's user belongs to (GET /api/v1/chat/channels): id, name, kind, members, last message, counts. Each row also carries the Team Chat 2.0 fields mentionCount (unread @mentions of you), notify (your stored notification level: all, mentions, mute, or null = the kind's default), updatedAt (when notify last changed, null = never), membershipId (your member row; changes after a removal and re-add), membershipSince (when this membership began) and membershipSeq (strictly increasing incarnation number). SIDE EFFECT: it also creates the caller's memberships in the account's AUTO channels (needs-a-hand, new-leads, wins) the first time, so a user who never opened Team Chat can read them. conduyt_list_chat_messages and conduyt_mark_chat_read already do this once for you when a channel listed by conduyt_get_chat_unread answers 403 'Not a member of this channel'; call this tool directly to discover channel ids.", {}, async () => {
46
+ strictInput(server.tool("conduyt_list_chat_channels", "List the team chat channels the API key's user belongs to (GET /api/v1/chat/channels): id, name, kind, members, last message, counts. Each row also carries the Team Chat 2.0 fields mentionCount (unread @mentions of you), notify (your stored notification level: all, mentions, mute, or null = the kind's default), updatedAt (when notify last changed, null = never), membershipId (your member row; changes after a removal and re-add), membershipSince (when this membership began) and membershipSeq (strictly increasing incarnation number). Each row also carries agentsRespond (boolean, #613 lane A: false = an admin muted AI teammates in that channel) and, ONLY while the account's AI teammates switch is on, aiTeammates: [{ userId, lastHeartbeatAt, replying }] (the paired AI teammates that are members of the channel; lastHeartbeatAt is their last listener heartbeat or null, replying is true while one is working a turn in that channel; derive the state like the app: heartbeat within 60 seconds = Listening, listening and replying = Replying, otherwise Away). The aiTeammates key is absent when the switch is off. SIDE EFFECT: it also creates the caller's memberships in the account's AUTO channels (needs-a-hand, new-leads, wins) the first time, so a user who never opened Team Chat can read them. conduyt_list_chat_messages and conduyt_mark_chat_read already do this once for you when a channel listed by conduyt_get_chat_unread answers 403 'Not a member of this channel'; call this tool directly to discover channel ids.", {}, async () => {
47
47
  const result = await client.get("/api/v1/chat/channels");
48
48
  return formatResult(result);
49
49
  }));
@@ -69,6 +69,36 @@ export function registerChatTools(server, client) {
69
69
  const result = await withMembershipBootstrap(client, () => client.get(`/api/v1/chat/channels/${channelId}/messages?${params}`));
70
70
  return formatResult(result);
71
71
  }));
72
+ 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).", {
73
+ channelId: z.string().uuid().describe("Channel UUID"),
74
+ body: z.string().trim().min(1).max(10_000).describe("Message text"),
75
+ 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."),
76
+ }, async ({ channelId, body, idempotencyKey }) => {
77
+ const result = await client.post(`/api/v1/chat/channels/${channelId}/messages`, { body, ...(idempotencyKey ? { idempotencyKey } : {}) });
78
+ return formatResult(result);
79
+ }));
80
+ 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).", {
81
+ channelId: z.string().uuid().describe("Channel UUID"),
82
+ parentId: z.string().uuid().describe("Top-level message UUID to reply to"),
83
+ body: z.string().trim().min(1).max(10_000).describe("Reply text"),
84
+ 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."),
85
+ 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."),
86
+ }, async ({ channelId, parentId, body, idempotencyKey, claimToken }) => {
87
+ // (#599 r7) a reply to an inbox turn (idempotencyKey reply:<source message id>) always names its claim
88
+ 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)) {
89
+ throw new Error("claimToken is required with an idempotencyKey reply:<source message id> (the turn prompt's reply claim token); nothing was posted");
90
+ }
91
+ const result = await client.post(`/api/v1/chat/channels/${channelId}/messages`, { body, parentId, ...(idempotencyKey ? { idempotencyKey } : {}), ...(claimToken ? { claimToken } : {}) });
92
+ return formatResult(result);
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.", {
95
+ channelId: z.string().uuid().describe("Channel UUID"),
96
+ messageId: z.string().uuid().describe("Message UUID"),
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 });
100
+ return formatResult(result);
101
+ }));
72
102
  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.", {
73
103
  channelId: z.string().uuid().describe("Channel UUID"),
74
104
  readUpTo: z.string().uuid().describe("UUID of the newest message you have seen in this channel"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conduyt-mcp",
3
- "version": "4.103.0",
3
+ "version": "4.104.0",
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",
@@ -130,11 +130,36 @@
130
130
  "status": "excluded",
131
131
  "reason": "UI-only deep-link helper (resolves a message to its channel/position for the app's router); not an agent surface"
132
132
  },
133
+ {
134
+ "match": "/api/v1/chat/agents/me/",
135
+ "status": "excluded",
136
+ "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"
137
+ },
138
+ {
139
+ "match": "POST /api/v1/chat/agents/pair",
140
+ "status": "excluded",
141
+ "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"
142
+ },
143
+ {
144
+ "match": "PATCH /api/v1/chat/channels/:id/agents",
145
+ "status": "excluded",
146
+ "reason": "(#599) session-only (assertSessionOnly): a channel's agents-respond mode is an admin setting; every API key is refused"
147
+ },
148
+ {
149
+ "match": "/api/v1/chat/agents",
150
+ "status": "excluded",
151
+ "reason": "(#599) session-only (assertSessionOnly): an admin manages AI teammates in Settings > AI; every API key is refused"
152
+ },
133
153
  {
134
154
  "match": "/api/v1/chat",
135
155
  "status": "planned",
136
156
  "reason": "team chat read/post for agents"
137
157
  },
158
+ {
159
+ "match": "/api/v1/settings/ai-teammates",
160
+ "status": "excluded",
161
+ "reason": "(#614) session-only (assertSessionOnly): an admin turns AI teammates on or off in Settings > AI (Professional and above, the trial, comp); every API key is refused"
162
+ },
138
163
  {
139
164
  "match": "/api/v1/settings",
140
165
  "status": "planned",
@@ -455,6 +480,11 @@
455
480
  "status": "planned",
456
481
  "reason": "account readiness/onboarding surfaces"
457
482
  },
483
+ {
484
+ "match": "GET /api/v1/activity/export.csv",
485
+ "status": "planned",
486
+ "reason": "activity-log CSV export (small586, landed on app main after the 4.103.0 pin); a CSV download, tool deferred; classified here so the 4.104.0 snapshot refresh passes the coverage gate"
487
+ },
458
488
  {
459
489
  "match": "/api/v1/activities",
460
490
  "status": "planned",
@@ -1,10 +1,10 @@
1
1
  {
2
- "generatedAt": "2026-10-29T23:02:33.248Z",
3
- "toolCount": 373,
4
- "totalEndpoints": 1118,
2
+ "generatedAt": "2026-10-30T13:37:28.694Z",
3
+ "toolCount": 376,
4
+ "totalEndpoints": 1134,
5
5
  "counts": {
6
- "covered": 327,
7
- "excluded": 336,
6
+ "covered": 329,
7
+ "excluded": 350,
8
8
  "planned": 455,
9
9
  "uncategorized": 0
10
10
  },
@@ -301,6 +301,10 @@
301
301
  "status": "planned",
302
302
  "reason": "activity detail reads beyond the log tools"
303
303
  },
304
+ "GET /api/v1/activity/export.csv": {
305
+ "status": "planned",
306
+ "reason": "activity-log CSV export (small586, landed on app main after the 4.103.0 pin); a CSV download, tool deferred; classified here so the 4.104.0 snapshot refresh passes the coverage gate"
307
+ },
304
308
  "GET /api/v1/admin/accounts": {
305
309
  "status": "excluded",
306
310
  "reason": "super-admin platform operations (Conduyt staff only)"
@@ -1269,6 +1273,46 @@
1269
1273
  "conduyt_voice_drop"
1270
1274
  ]
1271
1275
  },
1276
+ "GET /api/v1/chat/agents": {
1277
+ "status": "excluded",
1278
+ "reason": "(#599) session-only (assertSessionOnly): an admin manages AI teammates in Settings > AI; every API key is refused"
1279
+ },
1280
+ "POST /api/v1/chat/agents": {
1281
+ "status": "excluded",
1282
+ "reason": "(#599) session-only (assertSessionOnly): an admin manages AI teammates in Settings > AI; every API key is refused"
1283
+ },
1284
+ "PATCH /api/v1/chat/agents/:id": {
1285
+ "status": "excluded",
1286
+ "reason": "(#599) session-only (assertSessionOnly): an admin manages AI teammates in Settings > AI; every API key is refused"
1287
+ },
1288
+ "DELETE /api/v1/chat/agents/:id": {
1289
+ "status": "excluded",
1290
+ "reason": "(#599) session-only (assertSessionOnly): an admin manages AI teammates in Settings > AI; every API key is refused"
1291
+ },
1292
+ "POST /api/v1/chat/agents/:id/pair-code": {
1293
+ "status": "excluded",
1294
+ "reason": "(#599) session-only (assertSessionOnly): an admin manages AI teammates in Settings > AI; every API key is refused"
1295
+ },
1296
+ "POST /api/v1/chat/agents/me/heartbeat": {
1297
+ "status": "excluded",
1298
+ "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"
1299
+ },
1300
+ "GET /api/v1/chat/agents/me/inbox": {
1301
+ "status": "excluded",
1302
+ "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"
1303
+ },
1304
+ "POST /api/v1/chat/agents/me/inbox/ack": {
1305
+ "status": "excluded",
1306
+ "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"
1307
+ },
1308
+ "POST /api/v1/chat/agents/me/inbox/claim": {
1309
+ "status": "excluded",
1310
+ "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"
1311
+ },
1312
+ "POST /api/v1/chat/agents/pair": {
1313
+ "status": "excluded",
1314
+ "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"
1315
+ },
1272
1316
  "POST /api/v1/chat/ask": {
1273
1317
  "status": "planned",
1274
1318
  "reason": "team chat read/post for agents"
@@ -1287,6 +1331,10 @@
1287
1331
  "status": "planned",
1288
1332
  "reason": "team chat read/post for agents"
1289
1333
  },
1334
+ "PATCH /api/v1/chat/channels/:id/agents": {
1335
+ "status": "excluded",
1336
+ "reason": "(#599) session-only (assertSessionOnly): a channel's agents-respond mode is an admin setting; every API key is refused"
1337
+ },
1290
1338
  "POST /api/v1/chat/channels/:id/members": {
1291
1339
  "status": "planned",
1292
1340
  "reason": "team chat read/post for agents"
@@ -1303,8 +1351,11 @@
1303
1351
  ]
1304
1352
  },
1305
1353
  "POST /api/v1/chat/channels/:id/messages": {
1306
- "status": "planned",
1307
- "reason": "team chat read/post for agents"
1354
+ "status": "covered",
1355
+ "tools": [
1356
+ "conduyt_send_chat_message",
1357
+ "conduyt_reply_chat_message"
1358
+ ]
1308
1359
  },
1309
1360
  "GET /api/v1/chat/channels/:id/messages/:messageId": {
1310
1361
  "status": "planned",
@@ -1331,8 +1382,10 @@
1331
1382
  "reason": "team chat read/post for agents"
1332
1383
  },
1333
1384
  "POST /api/v1/chat/channels/:id/messages/:messageId/reactions": {
1334
- "status": "planned",
1335
- "reason": "team chat read/post for agents"
1385
+ "status": "covered",
1386
+ "tools": [
1387
+ "conduyt_react_chat_message"
1388
+ ]
1336
1389
  },
1337
1390
  "DELETE /api/v1/chat/channels/:id/messages/:messageId/reactions": {
1338
1391
  "status": "planned",
@@ -1440,6 +1493,10 @@
1440
1493
  "status": "planned",
1441
1494
  "reason": "company subroutes beyond core CRUD"
1442
1495
  },
1496
+ "GET /api/v1/companies/:id/related": {
1497
+ "status": "planned",
1498
+ "reason": "company subroutes beyond core CRUD"
1499
+ },
1443
1500
  "GET /api/v1/companies/:id/tags": {
1444
1501
  "status": "planned",
1445
1502
  "reason": "company subroutes beyond core CRUD"
@@ -1755,6 +1812,10 @@
1755
1812
  "status": "excluded",
1756
1813
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1757
1814
  },
1815
+ "GET /api/v1/cron/chat-webhooks": {
1816
+ "status": "excluded",
1817
+ "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
1818
+ },
1758
1819
  "GET /api/v1/cron/cleanup-audit-logs": {
1759
1820
  "status": "excluded",
1760
1821
  "reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
@@ -4240,6 +4301,14 @@
4240
4301
  "status": "planned",
4241
4302
  "reason": "account settings read/update surfaces"
4242
4303
  },
4304
+ "GET /api/v1/settings/ai-teammates": {
4305
+ "status": "excluded",
4306
+ "reason": "(#614) session-only (assertSessionOnly): an admin turns AI teammates on or off in Settings > AI (Professional and above, the trial, comp); every API key is refused"
4307
+ },
4308
+ "PATCH /api/v1/settings/ai-teammates": {
4309
+ "status": "excluded",
4310
+ "reason": "(#614) session-only (assertSessionOnly): an admin turns AI teammates on or off in Settings > AI (Professional and above, the trial, comp); every API key is refused"
4311
+ },
4243
4312
  "PUT /api/v1/settings/ai/byo": {
4244
4313
  "status": "planned",
4245
4314
  "reason": "account settings read/update surfaces"