@todoist/comms-mcp 0.0.1 → 11.0.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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +198 -34
  3. package/dist/comms-tool.d.ts +14 -0
  4. package/dist/comms-tool.js +1 -0
  5. package/dist/index.d.ts +1324 -0
  6. package/dist/index.js +55 -0
  7. package/dist/main.d.ts +2 -0
  8. package/dist/main.js +24 -0
  9. package/dist/mcp-helpers.d.ts +17 -0
  10. package/dist/mcp-helpers.js +40 -0
  11. package/dist/mcp-server.d.ts +10 -0
  12. package/dist/mcp-server.js +78 -0
  13. package/dist/tool-registry.d.ts +3 -0
  14. package/dist/tool-registry.js +49 -0
  15. package/dist/tools/build-link.d.ts +63 -0
  16. package/dist/tools/build-link.js +93 -0
  17. package/dist/tools/channel-output.d.ts +19 -0
  18. package/dist/tools/channel-output.js +47 -0
  19. package/dist/tools/create-channel.d.ts +55 -0
  20. package/dist/tools/create-channel.js +44 -0
  21. package/dist/tools/create-conversation.d.ts +49 -0
  22. package/dist/tools/create-conversation.js +75 -0
  23. package/dist/tools/create-thread.d.ts +67 -0
  24. package/dist/tools/create-thread.js +126 -0
  25. package/dist/tools/delete-object.d.ts +44 -0
  26. package/dist/tools/delete-object.js +84 -0
  27. package/dist/tools/fetch-inbox.d.ts +91 -0
  28. package/dist/tools/fetch-inbox.js +227 -0
  29. package/dist/tools/get-groups.d.ts +62 -0
  30. package/dist/tools/get-groups.js +116 -0
  31. package/dist/tools/get-mentions.d.ts +97 -0
  32. package/dist/tools/get-mentions.js +126 -0
  33. package/dist/tools/get-users.d.ts +58 -0
  34. package/dist/tools/get-users.js +107 -0
  35. package/dist/tools/get-workspaces.d.ts +61 -0
  36. package/dist/tools/get-workspaces.js +120 -0
  37. package/dist/tools/list-channels.d.ts +47 -0
  38. package/dist/tools/list-channels.js +89 -0
  39. package/dist/tools/list-conversations.d.ts +66 -0
  40. package/dist/tools/list-conversations.js +212 -0
  41. package/dist/tools/load-conversation.d.ts +92 -0
  42. package/dist/tools/load-conversation.js +128 -0
  43. package/dist/tools/load-thread.d.ts +127 -0
  44. package/dist/tools/load-thread.js +175 -0
  45. package/dist/tools/mark-done.d.ts +111 -0
  46. package/dist/tools/mark-done.js +275 -0
  47. package/dist/tools/mark-read.d.ts +73 -0
  48. package/dist/tools/mark-read.js +214 -0
  49. package/dist/tools/react.d.ts +59 -0
  50. package/dist/tools/react.js +97 -0
  51. package/dist/tools/reply.d.ts +69 -0
  52. package/dist/tools/reply.js +122 -0
  53. package/dist/tools/search-content.d.ts +103 -0
  54. package/dist/tools/search-content.js +138 -0
  55. package/dist/tools/update-channel.d.ts +53 -0
  56. package/dist/tools/update-channel.js +45 -0
  57. package/dist/tools/update-object.d.ts +52 -0
  58. package/dist/tools/update-object.js +161 -0
  59. package/dist/tools/update-user-info.d.ts +55 -0
  60. package/dist/tools/update-user-info.js +270 -0
  61. package/dist/tools/user-info.d.ts +32 -0
  62. package/dist/tools/user-info.js +16 -0
  63. package/dist/tools/user-output.d.ts +9 -0
  64. package/dist/tools/user-output.js +28 -0
  65. package/dist/usage-tracking.d.ts +23 -0
  66. package/dist/usage-tracking.js +41 -0
  67. package/dist/utils/attachments.d.ts +24 -0
  68. package/dist/utils/attachments.js +57 -0
  69. package/dist/utils/concurrency.d.ts +20 -0
  70. package/dist/utils/concurrency.js +24 -0
  71. package/dist/utils/constants.d.ts +5 -0
  72. package/dist/utils/constants.js +5 -0
  73. package/dist/utils/degrade.d.ts +81 -0
  74. package/dist/utils/degrade.js +149 -0
  75. package/dist/utils/output-schemas.d.ts +1283 -0
  76. package/dist/utils/output-schemas.js +657 -0
  77. package/dist/utils/read-position.d.ts +21 -0
  78. package/dist/utils/read-position.js +37 -0
  79. package/dist/utils/search-results.d.ts +36 -0
  80. package/dist/utils/search-results.js +82 -0
  81. package/dist/utils/target-types.d.ts +114 -0
  82. package/dist/utils/target-types.js +58 -0
  83. package/dist/utils/test-helpers.d.ts +95 -0
  84. package/dist/utils/test-helpers.js +231 -0
  85. package/dist/utils/tool-names.d.ts +25 -0
  86. package/dist/utils/tool-names.js +25 -0
  87. package/dist/utils/url-helpers.d.ts +3 -0
  88. package/dist/utils/url-helpers.js +10 -0
  89. package/dist/utils/user-status.d.ts +8 -0
  90. package/dist/utils/user-status.js +33 -0
  91. package/dist/utils/validate-comms-token.d.ts +13 -0
  92. package/dist/utils/validate-comms-token.js +22 -0
  93. package/package.json +85 -8
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Returns the objIndex of the last comment in a thread, which is the read
3
+ * position that clears its unread marker.
4
+ *
5
+ * Comment objIndexes start at 0 for the thread post, so the last one is
6
+ * `commentCount - 1`. `lastObjIndex` carries that value directly; the
7
+ * count-based fallback covers records where the API omits it.
8
+ */
9
+ function getThreadLastObjIndex(thread) {
10
+ return thread.lastObjIndex ?? Math.max(thread.commentCount - 1, 0);
11
+ }
12
+ /**
13
+ * Marks a thread as read through its latest comment.
14
+ *
15
+ * `threads.markRead` takes the objIndex the caller has read up to; the unread
16
+ * marker moves to the comment after it. Passing 0 only covers the thread post,
17
+ * so any thread with comments stays unread and keeps its badge. The read
18
+ * position is resolved from the thread record rather than guessed: overshooting
19
+ * (e.g. `commentCount`) also clears the badge, but pushes the marker past the
20
+ * next comment so it would arrive already read.
21
+ */
22
+ export async function markThreadFullyRead(client, id) {
23
+ const thread = await client.threads.getThread(id);
24
+ await client.threads.markRead({ id, objIndex: getThreadLastObjIndex(thread) });
25
+ }
26
+ /**
27
+ * Marks a conversation as read through its latest message.
28
+ *
29
+ * The API rejects `conversations.markRead` without a position (`Either
30
+ * obj_index or message_id should be provided`), and `objIndex: 0` only covers
31
+ * the first message, so the conversation record is fetched for its
32
+ * `lastObjIndex`.
33
+ */
34
+ export async function markConversationFullyRead(client, id) {
35
+ const conversation = await client.conversations.getConversation(id);
36
+ await client.conversations.markRead({ id, objIndex: conversation.lastObjIndex });
37
+ }
@@ -0,0 +1,36 @@
1
+ import { type SearchResult } from '@doist/comms-sdk';
2
+ import type { SearchResultItem } from './output-schemas.js';
3
+ export type RawSearchResult = {
4
+ id: string;
5
+ content: string;
6
+ creatorId: number;
7
+ created: string;
8
+ } & ({
9
+ type: 'thread';
10
+ threadId: string;
11
+ commentId?: string;
12
+ channelId?: string;
13
+ } | {
14
+ type: 'conversation';
15
+ conversationId: string;
16
+ messageId: string;
17
+ });
18
+ /**
19
+ * Normalizes SDK search results into the discriminated shape shared by
20
+ * search-content and get-mentions. The search API only emits 'thread' and
21
+ * 'conversation' results, each with its container id set; a comment match is a
22
+ * 'thread' result with commentId set, a message match a 'conversation' result
23
+ * with messageId set. Results missing a required id are dropped with a logged
24
+ * warning.
25
+ */
26
+ export declare function toRawSearchResults(items: SearchResult[], toolName: string): RawSearchResult[];
27
+ /**
28
+ * Enriches raw search results with creator/channel names and permalinks.
29
+ * Comment deep-links require a channel (see build-link); without one the URL
30
+ * falls back to the plain thread link.
31
+ */
32
+ export declare function toSearchResultItems(results: RawSearchResult[], { workspaceId, userLookup, channelLookup, }: {
33
+ workspaceId: number;
34
+ userLookup: Record<number, string>;
35
+ channelLookup: Record<string, string>;
36
+ }): SearchResultItem[];
@@ -0,0 +1,82 @@
1
+ import { getFullCommsURL } from '@doist/comms-sdk';
2
+ /**
3
+ * Normalizes SDK search results into the discriminated shape shared by
4
+ * search-content and get-mentions. The search API only emits 'thread' and
5
+ * 'conversation' results, each with its container id set; a comment match is a
6
+ * 'thread' result with commentId set, a message match a 'conversation' result
7
+ * with messageId set. Results missing a required id are dropped with a logged
8
+ * warning.
9
+ */
10
+ export function toRawSearchResults(items, toolName) {
11
+ const results = items.flatMap((r) => {
12
+ const common = {
13
+ id: r.id,
14
+ content: r.snippet,
15
+ creatorId: r.snippetCreatorId,
16
+ created: r.snippetLastUpdated.toISOString(),
17
+ };
18
+ if (r.type === 'thread' && r.threadId != null) {
19
+ return [
20
+ {
21
+ ...common,
22
+ type: 'thread',
23
+ threadId: r.threadId,
24
+ commentId: r.commentId ?? undefined,
25
+ channelId: r.channelId ?? undefined,
26
+ },
27
+ ];
28
+ }
29
+ if (r.type === 'conversation' && r.conversationId != null && r.messageId != null) {
30
+ return [
31
+ {
32
+ ...common,
33
+ type: 'conversation',
34
+ conversationId: r.conversationId,
35
+ messageId: r.messageId,
36
+ },
37
+ ];
38
+ }
39
+ return [];
40
+ });
41
+ if (results.length < items.length) {
42
+ console.error(`${toolName}: dropped search result(s) missing a required id`, {
43
+ dropped: items.length - results.length,
44
+ });
45
+ }
46
+ return results;
47
+ }
48
+ /**
49
+ * Enriches raw search results with creator/channel names and permalinks.
50
+ * Comment deep-links require a channel (see build-link); without one the URL
51
+ * falls back to the plain thread link.
52
+ */
53
+ export function toSearchResultItems(results, { workspaceId, userLookup, channelLookup, }) {
54
+ return results.map((r) => {
55
+ const common = {
56
+ creatorName: userLookup[r.creatorId],
57
+ workspaceId,
58
+ };
59
+ if (r.type === 'thread') {
60
+ return {
61
+ ...r,
62
+ ...common,
63
+ channelName: r.channelId ? channelLookup[r.channelId] : undefined,
64
+ url: getFullCommsURL({
65
+ workspaceId,
66
+ threadId: r.threadId,
67
+ channelId: r.channelId,
68
+ commentId: r.channelId ? r.commentId : undefined,
69
+ }),
70
+ };
71
+ }
72
+ return {
73
+ ...r,
74
+ ...common,
75
+ url: getFullCommsURL({
76
+ workspaceId,
77
+ conversationId: r.conversationId,
78
+ messageId: r.messageId,
79
+ }),
80
+ };
81
+ });
82
+ }
@@ -0,0 +1,114 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Target types for reactions
4
+ */
5
+ export declare const ReactionTargetType: {
6
+ readonly values: readonly ["thread", "comment", "message"];
7
+ readonly schema: z.ZodEnum<{
8
+ thread: "thread";
9
+ comment: "comment";
10
+ message: "message";
11
+ }>;
12
+ };
13
+ export declare const ReactionTargetTypeSchema: z.ZodEnum<{
14
+ thread: "thread";
15
+ comment: "comment";
16
+ message: "message";
17
+ }>;
18
+ export type ReactionTargetType = z.infer<typeof ReactionTargetTypeSchema>;
19
+ /**
20
+ * Target types for update-object
21
+ */
22
+ export declare const UpdateTargetType: {
23
+ readonly values: readonly ["thread", "comment", "message"];
24
+ readonly schema: z.ZodEnum<{
25
+ thread: "thread";
26
+ comment: "comment";
27
+ message: "message";
28
+ }>;
29
+ };
30
+ export declare const UpdateTargetTypeSchema: z.ZodEnum<{
31
+ thread: "thread";
32
+ comment: "comment";
33
+ message: "message";
34
+ }>;
35
+ export type UpdateTargetType = z.infer<typeof UpdateTargetTypeSchema>;
36
+ /**
37
+ * Target types for delete-object
38
+ */
39
+ export declare const DeleteTargetType: {
40
+ readonly values: readonly ["thread", "comment", "message"];
41
+ readonly schema: z.ZodEnum<{
42
+ thread: "thread";
43
+ comment: "comment";
44
+ message: "message";
45
+ }>;
46
+ };
47
+ export declare const DeleteTargetTypeSchema: z.ZodEnum<{
48
+ thread: "thread";
49
+ comment: "comment";
50
+ message: "message";
51
+ }>;
52
+ export type DeleteTargetType = z.infer<typeof DeleteTargetTypeSchema>;
53
+ /**
54
+ * Target types for replies
55
+ */
56
+ export declare const ReplyTargetType: {
57
+ readonly values: readonly ["thread", "conversation"];
58
+ readonly schema: z.ZodEnum<{
59
+ thread: "thread";
60
+ conversation: "conversation";
61
+ }>;
62
+ };
63
+ export declare const ReplyTargetTypeSchema: z.ZodEnum<{
64
+ thread: "thread";
65
+ conversation: "conversation";
66
+ }>;
67
+ export type ReplyTargetType = z.infer<typeof ReplyTargetTypeSchema>;
68
+ /**
69
+ * Search scope types
70
+ */
71
+ export declare const SearchScope: {
72
+ readonly values: readonly ["workspace", "thread", "conversation"];
73
+ readonly schema: z.ZodEnum<{
74
+ thread: "thread";
75
+ conversation: "conversation";
76
+ workspace: "workspace";
77
+ }>;
78
+ };
79
+ export declare const SearchScopeSchema: z.ZodEnum<{
80
+ thread: "thread";
81
+ conversation: "conversation";
82
+ workspace: "workspace";
83
+ }>;
84
+ export type SearchScope = z.infer<typeof SearchScopeSchema>;
85
+ /**
86
+ * Mark done types
87
+ */
88
+ export declare const MarkDoneType: {
89
+ readonly values: readonly ["thread", "conversation"];
90
+ readonly schema: z.ZodEnum<{
91
+ thread: "thread";
92
+ conversation: "conversation";
93
+ }>;
94
+ };
95
+ export declare const MarkDoneTypeSchema: z.ZodEnum<{
96
+ thread: "thread";
97
+ conversation: "conversation";
98
+ }>;
99
+ export type MarkDoneType = z.infer<typeof MarkDoneTypeSchema>;
100
+ /**
101
+ * Mark read item types
102
+ */
103
+ export declare const MarkReadItemType: {
104
+ readonly values: readonly ["thread", "conversation"];
105
+ readonly schema: z.ZodEnum<{
106
+ thread: "thread";
107
+ conversation: "conversation";
108
+ }>;
109
+ };
110
+ export declare const MarkReadItemTypeSchema: z.ZodEnum<{
111
+ thread: "thread";
112
+ conversation: "conversation";
113
+ }>;
114
+ export type MarkReadItemType = z.infer<typeof MarkReadItemTypeSchema>;
@@ -0,0 +1,58 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Helper to create an enum schema with types.
4
+ * Requires at least 2 values (Zod enum requirement).
5
+ * Only allows valid Comms object types.
6
+ */
7
+ function createEnumSchema(values) {
8
+ return {
9
+ values,
10
+ schema: z.enum(values),
11
+ };
12
+ }
13
+ /**
14
+ * Target types for reactions
15
+ */
16
+ export const ReactionTargetType = createEnumSchema(['thread', 'comment', 'message']);
17
+ export const ReactionTargetTypeSchema = ReactionTargetType.schema;
18
+ /**
19
+ * Targets that can be both updated and deleted (mutating-object tools).
20
+ * Kept as a shared const so update-object and delete-object stay in lockstep
21
+ * if/when the supported surface changes.
22
+ */
23
+ const objectTargets = ['thread', 'comment', 'message'];
24
+ /**
25
+ * Target types for update-object
26
+ */
27
+ export const UpdateTargetType = createEnumSchema(objectTargets);
28
+ export const UpdateTargetTypeSchema = UpdateTargetType.schema;
29
+ /**
30
+ * Target types for delete-object
31
+ */
32
+ export const DeleteTargetType = createEnumSchema(objectTargets);
33
+ export const DeleteTargetTypeSchema = DeleteTargetType.schema;
34
+ /**
35
+ * Items that carry an unread marker and accept replies. Shared so the tools
36
+ * that act on "a thread or a conversation" stay in lockstep.
37
+ */
38
+ const threadOrConversation = ['thread', 'conversation'];
39
+ /**
40
+ * Target types for replies
41
+ */
42
+ export const ReplyTargetType = createEnumSchema(threadOrConversation);
43
+ export const ReplyTargetTypeSchema = ReplyTargetType.schema;
44
+ /**
45
+ * Search scope types
46
+ */
47
+ export const SearchScope = createEnumSchema(['workspace', 'thread', 'conversation']);
48
+ export const SearchScopeSchema = SearchScope.schema;
49
+ /**
50
+ * Mark done types
51
+ */
52
+ export const MarkDoneType = createEnumSchema(threadOrConversation);
53
+ export const MarkDoneTypeSchema = MarkDoneType.schema;
54
+ /**
55
+ * Mark read item types
56
+ */
57
+ export const MarkReadItemType = createEnumSchema(threadOrConversation);
58
+ export const MarkReadItemTypeSchema = MarkReadItemType.schema;
@@ -0,0 +1,95 @@
1
+ import type { Channel, Comment, Conversation, ConversationMessage, Thread, User, Workspace } from '@doist/comms-sdk';
2
+ /**
3
+ * Creates a mock Channel with all required properties and sensible defaults.
4
+ * Pass only the properties you want to override for your specific test.
5
+ */
6
+ export declare function createMockChannel(overrides?: Partial<Channel>): Channel;
7
+ /**
8
+ * Creates a mock Thread with all required properties and sensible defaults.
9
+ * Pass only the properties you want to override for your specific test.
10
+ */
11
+ export declare function createMockThread(overrides?: Partial<Thread>): Thread;
12
+ /**
13
+ * Creates a mock Comment with all required properties and sensible defaults.
14
+ * Pass only the properties you want to override for your specific test.
15
+ */
16
+ export declare function createMockComment(overrides?: Partial<Comment>): Comment;
17
+ /**
18
+ * Creates a mock Conversation with all required properties and sensible defaults.
19
+ * Pass only the properties you want to override for your specific test.
20
+ */
21
+ export declare function createMockConversation(overrides?: Partial<Conversation>): Conversation;
22
+ /**
23
+ * Creates a mock ConversationMessage with all required properties and sensible defaults.
24
+ * Pass only the properties you want to override for your specific test.
25
+ */
26
+ export declare function createMockConversationMessage(overrides?: Partial<ConversationMessage>): ConversationMessage;
27
+ /**
28
+ * Creates a mock User with all required properties and sensible defaults.
29
+ * Pass only the properties you want to override for your specific test.
30
+ */
31
+ export declare function createMockUser(overrides?: Partial<User>): User;
32
+ /**
33
+ * Creates a mock Workspace with all required properties and sensible defaults.
34
+ * Pass only the properties you want to override for your specific test.
35
+ */
36
+ export declare function createMockWorkspace(overrides?: Partial<Workspace>): Workspace;
37
+ /**
38
+ * Common error messages used across tests.
39
+ */
40
+ export declare const TEST_ERRORS: {
41
+ readonly API_RATE_LIMIT: "API Error: Rate limit exceeded";
42
+ readonly API_UNAUTHORIZED: "API Error: Unauthorized";
43
+ readonly THREAD_NOT_FOUND: "Thread not found";
44
+ readonly CONVERSATION_NOT_FOUND: "Conversation not found";
45
+ };
46
+ /**
47
+ * Extracts the text content from a tool output for snapshot testing.
48
+ * This allows tests to match against just the text content while tools return structured output.
49
+ */
50
+ export declare function extractTextContent(toolOutput: unknown): string;
51
+ /**
52
+ * Extracts the structured content from a tool output for testing: a tool's own result, or the
53
+ * MCP result registration builds from it, which carries it in `structuredContent`.
54
+ */
55
+ export declare function extractStructuredContent(output: object): Record<string, unknown>;
56
+ /**
57
+ * Common mock IDs used across tests for consistency.
58
+ *
59
+ * Channel/thread/comment/conversation/message/group IDs are opaque base58
60
+ * UUIDv7 strings in the Comms API; workspace/user IDs are numeric.
61
+ * The string IDs below are stable fixtures — not real base58 — and exist
62
+ * purely so tests have predictable values to assert on.
63
+ */
64
+ export declare const TEST_IDS: {
65
+ readonly THREAD_1: "thread-id-1";
66
+ readonly THREAD_2: "thread-id-2";
67
+ readonly THREAD_3: "thread-id-3";
68
+ readonly COMMENT_1: "comment-id-1";
69
+ readonly COMMENT_2: "comment-id-2";
70
+ readonly CONVERSATION_1: "conv-id-1";
71
+ readonly CONVERSATION_2: "conv-id-2";
72
+ readonly MESSAGE_1: "msg-id-1";
73
+ readonly MESSAGE_2: "msg-id-2";
74
+ readonly CHANNEL_1: "channel-id-1";
75
+ readonly GROUP_1: "group-id-1";
76
+ readonly GROUP_2: "group-id-2";
77
+ readonly WORKSPACE_1: 11111;
78
+ readonly WORKSPACE_2: 11112;
79
+ readonly USER_1: 22222;
80
+ readonly USER_2: 44444;
81
+ readonly USER_3: 55555;
82
+ };
83
+ /**
84
+ * Fixed date for consistent test snapshots.
85
+ * Use this instead of new Date() in tests to avoid snapshot drift.
86
+ */
87
+ export declare const TODAY: "2025-01-01";
88
+ /**
89
+ * A `getUsersByIds` mock implementation answering from `users`, with `null` for
90
+ * any other requested id — what the API returns for a user the workspace has no
91
+ * record of.
92
+ */
93
+ export declare function usersByIds<T extends {
94
+ id: number;
95
+ }>(users: readonly T[]): never;
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Creates a mock Channel with all required properties and sensible defaults.
3
+ * Pass only the properties you want to override for your specific test.
4
+ */
5
+ export function createMockChannel(overrides = {}) {
6
+ return {
7
+ id: TEST_IDS.CHANNEL_1,
8
+ name: 'General',
9
+ creator: TEST_IDS.USER_1,
10
+ public: true,
11
+ workspaceId: TEST_IDS.WORKSPACE_1,
12
+ archived: false,
13
+ created: new Date('2024-01-01T00:00:00Z'),
14
+ version: 1,
15
+ url: `https://comms.todoist.com/${TEST_IDS.WORKSPACE_1}/ch/${TEST_IDS.CHANNEL_1}/`,
16
+ ...overrides,
17
+ };
18
+ }
19
+ /**
20
+ * Creates a mock Thread with all required properties and sensible defaults.
21
+ * Pass only the properties you want to override for your specific test.
22
+ */
23
+ export function createMockThread(overrides = {}) {
24
+ return {
25
+ id: TEST_IDS.THREAD_1,
26
+ title: 'Test Thread',
27
+ content: 'Test thread content',
28
+ channelId: TEST_IDS.CHANNEL_1,
29
+ workspaceId: TEST_IDS.WORKSPACE_1,
30
+ creator: TEST_IDS.USER_1,
31
+ posted: new Date('2024-01-01T00:00:00Z'),
32
+ lastUpdated: new Date('2024-01-01T00:00:00Z'),
33
+ pinned: false,
34
+ snippet: 'Test thread content',
35
+ snippetCreator: TEST_IDS.USER_1,
36
+ systemMessage: null,
37
+ attachments: [],
38
+ groups: [],
39
+ reactions: {},
40
+ recipients: [],
41
+ commentCount: 0,
42
+ isArchived: false,
43
+ inInbox: true,
44
+ participants: [TEST_IDS.USER_1],
45
+ url: `https://comms.todoist.com/${TEST_IDS.WORKSPACE_1}/ch/${TEST_IDS.CHANNEL_1}/t/${TEST_IDS.THREAD_1}/`,
46
+ ...overrides,
47
+ };
48
+ }
49
+ /**
50
+ * Creates a mock Comment with all required properties and sensible defaults.
51
+ * Pass only the properties you want to override for your specific test.
52
+ */
53
+ export function createMockComment(overrides = {}) {
54
+ return {
55
+ id: TEST_IDS.COMMENT_1,
56
+ content: 'Test comment content',
57
+ threadId: TEST_IDS.THREAD_1,
58
+ workspaceId: TEST_IDS.WORKSPACE_1,
59
+ channelId: TEST_IDS.CHANNEL_1,
60
+ creator: TEST_IDS.USER_1,
61
+ posted: new Date('2024-01-01T00:00:00Z'),
62
+ systemMessage: null,
63
+ attachments: [],
64
+ reactions: {},
65
+ objIndex: 1,
66
+ url: `https://comms.todoist.com/${TEST_IDS.WORKSPACE_1}/ch/${TEST_IDS.CHANNEL_1}/t/${TEST_IDS.THREAD_1}/c/${TEST_IDS.COMMENT_1}`,
67
+ ...overrides,
68
+ };
69
+ }
70
+ /**
71
+ * Creates a mock Conversation with all required properties and sensible defaults.
72
+ * Pass only the properties you want to override for your specific test.
73
+ */
74
+ export function createMockConversation(overrides = {}) {
75
+ return {
76
+ id: TEST_IDS.CONVERSATION_1,
77
+ workspaceId: TEST_IDS.WORKSPACE_1,
78
+ userIds: [TEST_IDS.USER_1, TEST_IDS.USER_2],
79
+ messageCount: 0,
80
+ lastObjIndex: 0,
81
+ snippet: '',
82
+ snippetCreators: [],
83
+ archived: false,
84
+ creator: TEST_IDS.USER_1,
85
+ created: new Date('2024-01-01T00:00:00Z'),
86
+ lastActive: new Date('2024-01-01T00:00:00Z'),
87
+ url: `https://comms.todoist.com/${TEST_IDS.WORKSPACE_1}/msg/${TEST_IDS.CONVERSATION_1}/`,
88
+ ...overrides,
89
+ };
90
+ }
91
+ /**
92
+ * Creates a mock ConversationMessage with all required properties and sensible defaults.
93
+ * Pass only the properties you want to override for your specific test.
94
+ */
95
+ export function createMockConversationMessage(overrides = {}) {
96
+ return {
97
+ id: TEST_IDS.MESSAGE_1,
98
+ content: 'Test message content',
99
+ creator: TEST_IDS.USER_1,
100
+ conversationId: TEST_IDS.CONVERSATION_1,
101
+ workspaceId: TEST_IDS.WORKSPACE_1,
102
+ posted: new Date('2024-01-01T00:00:00Z'),
103
+ systemMessage: null,
104
+ attachments: [],
105
+ reactions: {},
106
+ objIndex: 1,
107
+ lastEdited: null,
108
+ url: `https://comms.todoist.com/${TEST_IDS.WORKSPACE_1}/msg/${TEST_IDS.CONVERSATION_1}/m/${TEST_IDS.MESSAGE_1}`,
109
+ ...overrides,
110
+ };
111
+ }
112
+ /**
113
+ * Creates a mock User with all required properties and sensible defaults.
114
+ * Pass only the properties you want to override for your specific test.
115
+ */
116
+ export function createMockUser(overrides = {}) {
117
+ return {
118
+ id: TEST_IDS.USER_1,
119
+ email: 'test@example.com',
120
+ fullName: 'Test User',
121
+ shortName: 'Test',
122
+ timezone: 'UTC',
123
+ lang: 'en',
124
+ removed: false,
125
+ ...overrides,
126
+ };
127
+ }
128
+ /**
129
+ * Creates a mock Workspace with all required properties and sensible defaults.
130
+ * Pass only the properties you want to override for your specific test.
131
+ */
132
+ export function createMockWorkspace(overrides = {}) {
133
+ return {
134
+ id: TEST_IDS.WORKSPACE_1,
135
+ name: 'Test Workspace',
136
+ creator: TEST_IDS.USER_1,
137
+ created: new Date('2024-01-01T00:00:00Z'),
138
+ defaultConversation: TEST_IDS.CONVERSATION_1,
139
+ plan: 'free',
140
+ ...overrides,
141
+ };
142
+ }
143
+ /**
144
+ * Common error messages used across tests.
145
+ */
146
+ export const TEST_ERRORS = {
147
+ API_RATE_LIMIT: 'API Error: Rate limit exceeded',
148
+ API_UNAUTHORIZED: 'API Error: Unauthorized',
149
+ THREAD_NOT_FOUND: 'Thread not found',
150
+ CONVERSATION_NOT_FOUND: 'Conversation not found',
151
+ };
152
+ /**
153
+ * Extracts the text content from a tool output for snapshot testing.
154
+ * This allows tests to match against just the text content while tools return structured output.
155
+ */
156
+ export function extractTextContent(toolOutput) {
157
+ if (typeof toolOutput === 'string') {
158
+ return toolOutput;
159
+ }
160
+ // A tool's own result, as `execute` returns it.
161
+ if (typeof toolOutput === 'object' &&
162
+ toolOutput !== null &&
163
+ 'textContent' in toolOutput &&
164
+ typeof toolOutput.textContent === 'string') {
165
+ return toolOutput.textContent;
166
+ }
167
+ if (typeof toolOutput === 'object' && toolOutput !== null && 'content' in toolOutput) {
168
+ const output = toolOutput;
169
+ if (Array.isArray(output.content) &&
170
+ output.content[0] &&
171
+ typeof output.content[0] === 'object' &&
172
+ output.content[0] !== null &&
173
+ 'type' in output.content[0] &&
174
+ 'text' in output.content[0] &&
175
+ output.content[0].type === 'text') {
176
+ return output.content[0].text;
177
+ }
178
+ }
179
+ throw new Error('Expected tool output to have text content');
180
+ }
181
+ /**
182
+ * Extracts the structured content from a tool output for testing: a tool's own result, or the
183
+ * MCP result registration builds from it, which carries it in `structuredContent`.
184
+ */
185
+ export function extractStructuredContent(output) {
186
+ if ('structuredContent' in output && typeof output.structuredContent === 'object') {
187
+ return output.structuredContent;
188
+ }
189
+ throw new Error('Expected tool output to have structured content');
190
+ }
191
+ /**
192
+ * Common mock IDs used across tests for consistency.
193
+ *
194
+ * Channel/thread/comment/conversation/message/group IDs are opaque base58
195
+ * UUIDv7 strings in the Comms API; workspace/user IDs are numeric.
196
+ * The string IDs below are stable fixtures — not real base58 — and exist
197
+ * purely so tests have predictable values to assert on.
198
+ */
199
+ export const TEST_IDS = {
200
+ THREAD_1: 'thread-id-1',
201
+ THREAD_2: 'thread-id-2',
202
+ THREAD_3: 'thread-id-3',
203
+ COMMENT_1: 'comment-id-1',
204
+ COMMENT_2: 'comment-id-2',
205
+ CONVERSATION_1: 'conv-id-1',
206
+ CONVERSATION_2: 'conv-id-2',
207
+ MESSAGE_1: 'msg-id-1',
208
+ MESSAGE_2: 'msg-id-2',
209
+ CHANNEL_1: 'channel-id-1',
210
+ GROUP_1: 'group-id-1',
211
+ GROUP_2: 'group-id-2',
212
+ WORKSPACE_1: 11111,
213
+ WORKSPACE_2: 11112,
214
+ USER_1: 22222,
215
+ USER_2: 44444,
216
+ USER_3: 55555,
217
+ };
218
+ /**
219
+ * Fixed date for consistent test snapshots.
220
+ * Use this instead of new Date() in tests to avoid snapshot drift.
221
+ */
222
+ export const TODAY = '2025-01-01';
223
+ /**
224
+ * A `getUsersByIds` mock implementation answering from `users`, with `null` for
225
+ * any other requested id — what the API returns for a user the workspace has no
226
+ * record of.
227
+ */
228
+ export function usersByIds(users) {
229
+ const byId = new Map(users.map((user) => [user.id, user]));
230
+ return (async ({ userIds }) => Object.fromEntries(userIds.map((id) => [id, byId.get(id) ?? null])));
231
+ }
@@ -0,0 +1,25 @@
1
+ export declare const ToolNames: {
2
+ readonly USER_INFO: "user-info";
3
+ readonly UPDATE_USER_INFO: "update-user-info";
4
+ readonly FETCH_INBOX: "fetch-inbox";
5
+ readonly LOAD_THREAD: "load-thread";
6
+ readonly LOAD_CONVERSATION: "load-conversation";
7
+ readonly SEARCH_CONTENT: "search-content";
8
+ readonly GET_MENTIONS: "get-mentions";
9
+ readonly CREATE_CHANNEL: "create-channel";
10
+ readonly UPDATE_CHANNEL: "update-channel";
11
+ readonly CREATE_THREAD: "create-thread";
12
+ readonly CREATE_CONVERSATION: "create-conversation";
13
+ readonly UPDATE_OBJECT: "update-object";
14
+ readonly DELETE_OBJECT: "delete-object";
15
+ readonly REPLY: "reply";
16
+ readonly REACT: "react";
17
+ readonly MARK_DONE: "mark-done";
18
+ readonly MARK_READ: "mark-read";
19
+ readonly BUILD_LINK: "build-link";
20
+ readonly GET_WORKSPACES: "get-workspaces";
21
+ readonly GET_USERS: "get-users";
22
+ readonly GET_GROUPS: "get-groups";
23
+ readonly LIST_CHANNELS: "list-channels";
24
+ readonly LIST_CONVERSATIONS: "list-conversations";
25
+ };