conduyt-mcp 4.77.0 → 4.78.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 +11 -1
- package/dist/index.js +2 -0
- package/dist/tools/chat.d.ts +6 -0
- package/dist/tools/chat.js +55 -0
- package/dist/tools/notifications.js +1 -1
- package/package.json +1 -1
- package/scripts/coverage-manifest.json +5 -0
- package/scripts/coverage-matrix.json +25 -10
package/README.md
CHANGED
|
@@ -197,9 +197,19 @@ The Mailchimp OAuth connect and callback are browser-only: connect Mailchimp in
|
|
|
197
197
|
### Notifications
|
|
198
198
|
| Tool | Description |
|
|
199
199
|
|------|-------------|
|
|
200
|
-
| `conduyt_list_notifications` | List notifications for current user |
|
|
200
|
+
| `conduyt_list_notifications` | List notifications for current user; `meta.latestByType` is `{type: {latest, count}}` over the last 24 h (newest createdAt + row count per type, read state ignored) next to `unreadCount` / `unreadByType` |
|
|
201
201
|
| `conduyt_create_notification` | Send in-app + push notification to a team member |
|
|
202
202
|
|
|
203
|
+
### Team Chat
|
|
204
|
+
| Tool | Description |
|
|
205
|
+
|------|-------------|
|
|
206
|
+
| `conduyt_list_chat_channels` | `GET /chat/channels`: your channels; also CREATES your memberships in the auto channels the first time |
|
|
207
|
+
| `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) |
|
|
208
|
+
| `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 |
|
|
209
|
+
| `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) |
|
|
210
|
+
|
|
211
|
+
The rest of team chat (sending, reactions, pins, presence, uploads) is not wrapped yet; `GET /chat/messages/{messageId}/location` is a UI deep-link helper and deliberately not wrapped.
|
|
212
|
+
|
|
203
213
|
### Custom Fields
|
|
204
214
|
| Tool | Description |
|
|
205
215
|
|------|-------------|
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import { registerBoardSettingsTools } from "./tools/board-settings.js";
|
|
|
26
26
|
import { registerMailchimpTools } from "./tools/mailchimp.js";
|
|
27
27
|
import { registerStatusMetadataTools } from "./tools/status-metadata.js";
|
|
28
28
|
import { registerNotificationTools } from "./tools/notifications.js";
|
|
29
|
+
import { registerChatTools } from "./tools/chat.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";
|
|
@@ -102,6 +103,7 @@ registerBoardSettingsTools(server, client);
|
|
|
102
103
|
registerMailchimpTools(server, client);
|
|
103
104
|
registerStatusMetadataTools(server, client);
|
|
104
105
|
registerNotificationTools(server, client);
|
|
106
|
+
registerChatTools(server, client);
|
|
105
107
|
registerCustomFieldTools(server, client);
|
|
106
108
|
registerSmartListTools(server, client);
|
|
107
109
|
registerUserTools(server, client);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { ConduytClient } from "../client.js";
|
|
3
|
+
/** The messages route's membership refusal (403 "Not a member of this channel"); any other 403 (a permission) is NOT this. */
|
|
4
|
+
export declare function isMembershipRefusal(err: unknown): boolean;
|
|
5
|
+
/** Team chat read side (app 10-09, live sha 3869c79ba): the unread vector, messages carrying `seq`, and the read acknowledgement. */
|
|
6
|
+
export declare function registerChatTools(server: McpServer, client: ConduytClient): void;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ConduytApiError, formatResult } from "../client.js";
|
|
3
|
+
import { strictInput } from "./strict.js";
|
|
4
|
+
/** The messages route's membership refusal (403 "Not a member of this channel"); any other 403 (a permission) is NOT this. */
|
|
5
|
+
export function isMembershipRefusal(err) {
|
|
6
|
+
return err instanceof Error && /\b403\b/.test(err.message) && /Not a member of this channel/.test(err.message)
|
|
7
|
+
&& (!(err instanceof ConduytApiError) || err.status === 403);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* GET /chat/unread lists the AUTO channels even when the caller has no membership row yet, while the messages route answers
|
|
11
|
+
* 403 without one; GET /chat/channels is what lazily creates it. So on that one refusal: call GET /chat/channels ONCE, retry
|
|
12
|
+
* the read ONCE. Bounded: a second refusal, or a 403 for any other reason, reaches the caller untouched.
|
|
13
|
+
*/
|
|
14
|
+
async function withMembershipBootstrap(client, read) {
|
|
15
|
+
try {
|
|
16
|
+
return await read();
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
if (!isMembershipRefusal(err))
|
|
20
|
+
throw err;
|
|
21
|
+
await client.get("/api/v1/chat/channels");
|
|
22
|
+
return read();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Team chat read side (app 10-09, live sha 3869c79ba): the unread vector, messages carrying `seq`, and the read acknowledgement. */
|
|
26
|
+
export function registerChatTools(server, client) {
|
|
27
|
+
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. 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 () => {
|
|
28
|
+
const result = await client.get("/api/v1/chat/channels");
|
|
29
|
+
return formatResult(result);
|
|
30
|
+
}));
|
|
31
|
+
strictInput(server.tool("conduyt_get_chat_unread", "Team chat unread vector for the API key's user (GET /api/v1/chat/unread): one row per channel the user is a member of, { channelId, unread, soundUnread, newestSeq, latestSeq }. unread is the badge count (auto channels are 0 by design); soundUnread counts unread messages in every channel, auto ones included; newestSeq is the seq of the channel's newest message not written by the user (null = none, panic cards left out); latestSeq is the same including panic cards. Read-only: no writes, never creates a membership, so a row can be an AUTO channel the caller has not opened yet (no membership row: reading it first answers 403, which conduyt_list_chat_messages / conduyt_mark_chat_read recover from by calling conduyt_list_chat_channels once and retrying once). Use it to decide which channel to read next, then conduyt_list_chat_messages.", {}, async () => {
|
|
32
|
+
const result = await client.get("/api/v1/chat/unread");
|
|
33
|
+
return formatResult(result);
|
|
34
|
+
}));
|
|
35
|
+
strictInput(server.tool("conduyt_list_chat_messages", "List messages in a team chat channel (GET /api/v1/chat/channels/{id}/messages), newest first, paged by cursor. The caller must be a channel member (else 403). Every message carries seq, its commit-ordered position inside the channel (a total order: the list and the cursor follow it). Read-only: this tool does not move the read marker. On a 403 'Not a member of this channel' it calls GET /chat/channels once (which creates the caller's auto-channel memberships) and retries ONCE; a second refusal, or a 403 for any other reason, is returned as is; acknowledge with conduyt_mark_chat_read.", {
|
|
36
|
+
channelId: z.string().uuid().describe("Channel UUID"),
|
|
37
|
+
limit: z.number().int().min(1).max(100).optional().describe("Messages per page (1-100, default 50)"),
|
|
38
|
+
cursor: z.string().uuid().optional().describe("nextCursor from the previous page (a message UUID)"),
|
|
39
|
+
}, async ({ channelId, limit, cursor }) => {
|
|
40
|
+
const params = new URLSearchParams();
|
|
41
|
+
if (limit !== undefined)
|
|
42
|
+
params.set("limit", String(limit));
|
|
43
|
+
if (cursor !== undefined)
|
|
44
|
+
params.set("cursor", cursor);
|
|
45
|
+
const result = await withMembershipBootstrap(client, () => client.get(`/api/v1/chat/channels/${channelId}/messages?${params}`));
|
|
46
|
+
return formatResult(result);
|
|
47
|
+
}));
|
|
48
|
+
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.", {
|
|
49
|
+
channelId: z.string().uuid().describe("Channel UUID"),
|
|
50
|
+
readUpTo: z.string().uuid().describe("UUID of the newest message you have seen in this channel"),
|
|
51
|
+
}, async ({ channelId, readUpTo }) => {
|
|
52
|
+
const result = await withMembershipBootstrap(client, () => client.get(`/api/v1/chat/channels/${channelId}/messages?limit=1&readUpTo=${readUpTo}`));
|
|
53
|
+
return formatResult(result);
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { formatResult } from "../client.js";
|
|
3
3
|
export function registerNotificationTools(server, client) {
|
|
4
|
-
server.tool("conduyt_list_notifications", "List notifications for the current user (determined by API key's associated user).", {
|
|
4
|
+
server.tool("conduyt_list_notifications", "List notifications for the current user (determined by API key's associated user). The response meta carries page, per_page, total, unreadCount, unreadByType ({type: count}) and latestByType ({type: {latest, count}}: per type over the last 24 hours, the newest createdAt as an ISO string and the row count, read state ignored; a poller sees a type advance when latest moves or count rises).", {
|
|
5
5
|
page: z.number().optional().describe("Page number (default 1)"),
|
|
6
6
|
per_page: z.number().optional().describe("Results per page"),
|
|
7
7
|
}, async ({ page, per_page }) => {
|
package/package.json
CHANGED
|
@@ -115,6 +115,11 @@
|
|
|
115
115
|
"status": "planned",
|
|
116
116
|
"reason": "file listing/metadata is agent-useful; upload stays browser-side"
|
|
117
117
|
},
|
|
118
|
+
{
|
|
119
|
+
"match": "GET /api/v1/chat/messages/",
|
|
120
|
+
"status": "excluded",
|
|
121
|
+
"reason": "UI-only deep-link helper (resolves a message to its channel/position for the app's router); not an agent surface"
|
|
122
|
+
},
|
|
118
123
|
{
|
|
119
124
|
"match": "/api/v1/chat",
|
|
120
125
|
"status": "planned",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-10-
|
|
3
|
-
"toolCount":
|
|
4
|
-
"totalEndpoints":
|
|
2
|
+
"generatedAt": "2026-10-09T04:00:29.593Z",
|
|
3
|
+
"toolCount": 334,
|
|
4
|
+
"totalEndpoints": 1066,
|
|
5
5
|
"counts": {
|
|
6
|
-
"covered":
|
|
7
|
-
"excluded":
|
|
8
|
-
"planned":
|
|
6
|
+
"covered": 299,
|
|
7
|
+
"excluded": 319,
|
|
8
|
+
"planned": 448,
|
|
9
9
|
"uncategorized": 0
|
|
10
10
|
},
|
|
11
11
|
"matrix": {
|
|
@@ -1235,8 +1235,10 @@
|
|
|
1235
1235
|
"reason": "team chat read/post for agents"
|
|
1236
1236
|
},
|
|
1237
1237
|
"GET /api/v1/chat/channels": {
|
|
1238
|
-
"status": "
|
|
1239
|
-
"
|
|
1238
|
+
"status": "covered",
|
|
1239
|
+
"tools": [
|
|
1240
|
+
"conduyt_list_chat_channels"
|
|
1241
|
+
]
|
|
1240
1242
|
},
|
|
1241
1243
|
"POST /api/v1/chat/channels": {
|
|
1242
1244
|
"status": "planned",
|
|
@@ -1251,8 +1253,11 @@
|
|
|
1251
1253
|
"reason": "team chat read/post for agents"
|
|
1252
1254
|
},
|
|
1253
1255
|
"GET /api/v1/chat/channels/:id/messages": {
|
|
1254
|
-
"status": "
|
|
1255
|
-
"
|
|
1256
|
+
"status": "covered",
|
|
1257
|
+
"tools": [
|
|
1258
|
+
"conduyt_list_chat_messages",
|
|
1259
|
+
"conduyt_mark_chat_read"
|
|
1260
|
+
]
|
|
1256
1261
|
},
|
|
1257
1262
|
"POST /api/v1/chat/channels/:id/messages": {
|
|
1258
1263
|
"status": "planned",
|
|
@@ -1318,6 +1323,10 @@
|
|
|
1318
1323
|
"status": "planned",
|
|
1319
1324
|
"reason": "team chat read/post for agents"
|
|
1320
1325
|
},
|
|
1326
|
+
"GET /api/v1/chat/messages/:messageId/location": {
|
|
1327
|
+
"status": "excluded",
|
|
1328
|
+
"reason": "UI-only deep-link helper (resolves a message to its channel/position for the app's router); not an agent surface"
|
|
1329
|
+
},
|
|
1321
1330
|
"GET /api/v1/chat/presence": {
|
|
1322
1331
|
"status": "planned",
|
|
1323
1332
|
"reason": "team chat read/post for agents"
|
|
@@ -1326,6 +1335,12 @@
|
|
|
1326
1335
|
"status": "planned",
|
|
1327
1336
|
"reason": "team chat read/post for agents"
|
|
1328
1337
|
},
|
|
1338
|
+
"GET /api/v1/chat/unread": {
|
|
1339
|
+
"status": "covered",
|
|
1340
|
+
"tools": [
|
|
1341
|
+
"conduyt_get_chat_unread"
|
|
1342
|
+
]
|
|
1343
|
+
},
|
|
1329
1344
|
"GET /api/v1/companies": {
|
|
1330
1345
|
"status": "covered",
|
|
1331
1346
|
"tools": [
|