@beeper/desktop-mcp 0.1.5 → 4.2.1

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 (227) hide show
  1. package/README.md +10 -14
  2. package/docs-search-tool.d.mts +44 -0
  3. package/docs-search-tool.d.mts.map +1 -0
  4. package/docs-search-tool.d.ts +44 -0
  5. package/docs-search-tool.d.ts.map +1 -0
  6. package/docs-search-tool.js +43 -0
  7. package/docs-search-tool.js.map +1 -0
  8. package/docs-search-tool.mjs +39 -0
  9. package/docs-search-tool.mjs.map +1 -0
  10. package/handlers/get-accounts.d.mts +3 -0
  11. package/handlers/get-accounts.d.mts.map +1 -0
  12. package/handlers/get-accounts.d.ts +3 -0
  13. package/handlers/get-accounts.d.ts.map +1 -0
  14. package/handlers/get-accounts.js +32 -0
  15. package/handlers/get-accounts.js.map +1 -0
  16. package/handlers/get-accounts.mjs +28 -0
  17. package/handlers/get-accounts.mjs.map +1 -0
  18. package/handlers/get-chat.d.mts +3 -0
  19. package/handlers/get-chat.d.mts.map +1 -0
  20. package/handlers/get-chat.d.ts +3 -0
  21. package/handlers/get-chat.d.ts.map +1 -0
  22. package/handlers/get-chat.js +20 -0
  23. package/handlers/get-chat.js.map +1 -0
  24. package/handlers/get-chat.mjs +16 -0
  25. package/handlers/get-chat.mjs.map +1 -0
  26. package/handlers/index.d.mts +3 -0
  27. package/handlers/index.d.mts.map +1 -0
  28. package/handlers/index.d.ts +3 -0
  29. package/handlers/index.d.ts.map +1 -0
  30. package/handlers/index.js +30 -0
  31. package/handlers/index.js.map +1 -0
  32. package/handlers/index.mjs +27 -0
  33. package/handlers/index.mjs.map +1 -0
  34. package/handlers/list-chats.d.mts +3 -0
  35. package/handlers/list-chats.d.mts.map +1 -0
  36. package/handlers/list-chats.d.ts +3 -0
  37. package/handlers/list-chats.d.ts.map +1 -0
  38. package/handlers/list-chats.js +46 -0
  39. package/handlers/list-chats.js.map +1 -0
  40. package/handlers/list-chats.mjs +42 -0
  41. package/handlers/list-chats.mjs.map +1 -0
  42. package/handlers/list-messages.d.mts +3 -0
  43. package/handlers/list-messages.d.mts.map +1 -0
  44. package/handlers/list-messages.d.ts +3 -0
  45. package/handlers/list-messages.d.ts.map +1 -0
  46. package/handlers/list-messages.js +34 -0
  47. package/handlers/list-messages.js.map +1 -0
  48. package/handlers/list-messages.mjs +30 -0
  49. package/handlers/list-messages.mjs.map +1 -0
  50. package/handlers/open-app.d.mts +3 -0
  51. package/handlers/open-app.d.mts.map +1 -0
  52. package/handlers/open-app.d.ts +3 -0
  53. package/handlers/open-app.d.ts.map +1 -0
  54. package/handlers/open-app.js +26 -0
  55. package/handlers/open-app.js.map +1 -0
  56. package/handlers/open-app.mjs +22 -0
  57. package/handlers/open-app.mjs.map +1 -0
  58. package/handlers/search-chats.d.mts +3 -0
  59. package/handlers/search-chats.d.mts.map +1 -0
  60. package/handlers/search-chats.d.ts +3 -0
  61. package/handlers/search-chats.d.ts.map +1 -0
  62. package/handlers/search-chats.js +38 -0
  63. package/handlers/search-chats.js.map +1 -0
  64. package/handlers/search-chats.mjs +34 -0
  65. package/handlers/search-chats.mjs.map +1 -0
  66. package/handlers/search-messages.d.mts +3 -0
  67. package/handlers/search-messages.d.mts.map +1 -0
  68. package/handlers/search-messages.d.ts +3 -0
  69. package/handlers/search-messages.d.ts.map +1 -0
  70. package/handlers/search-messages.js +11 -0
  71. package/handlers/search-messages.js.map +1 -0
  72. package/handlers/search-messages.mjs +7 -0
  73. package/handlers/search-messages.mjs.map +1 -0
  74. package/handlers/search.d.mts +3 -0
  75. package/handlers/search.d.mts.map +1 -0
  76. package/handlers/search.d.ts +3 -0
  77. package/handlers/search.d.ts.map +1 -0
  78. package/handlers/search.js +29 -0
  79. package/handlers/search.js.map +1 -0
  80. package/handlers/search.mjs +25 -0
  81. package/handlers/search.mjs.map +1 -0
  82. package/handlers/send-message.d.mts +3 -0
  83. package/handlers/send-message.d.mts.map +1 -0
  84. package/handlers/send-message.d.ts +3 -0
  85. package/handlers/send-message.d.ts.map +1 -0
  86. package/handlers/send-message.js +20 -0
  87. package/handlers/send-message.js.map +1 -0
  88. package/handlers/send-message.mjs +16 -0
  89. package/handlers/send-message.mjs.map +1 -0
  90. package/handlers/utils.d.mts +29 -0
  91. package/handlers/utils.d.mts.map +1 -0
  92. package/handlers/utils.d.ts +29 -0
  93. package/handlers/utils.d.ts.map +1 -0
  94. package/handlers/utils.js +296 -0
  95. package/handlers/utils.js.map +1 -0
  96. package/handlers/utils.mjs +282 -0
  97. package/handlers/utils.mjs.map +1 -0
  98. package/http.d.mts +6 -0
  99. package/http.d.mts.map +1 -1
  100. package/http.d.ts +6 -0
  101. package/http.d.ts.map +1 -1
  102. package/http.js +7 -4
  103. package/http.js.map +1 -1
  104. package/http.mjs +3 -3
  105. package/http.mjs.map +1 -1
  106. package/options.d.mts +1 -0
  107. package/options.d.mts.map +1 -1
  108. package/options.d.ts +1 -0
  109. package/options.d.ts.map +1 -1
  110. package/options.js +13 -6
  111. package/options.js.map +1 -1
  112. package/options.mjs +13 -6
  113. package/options.mjs.map +1 -1
  114. package/package.json +23 -2
  115. package/server.d.mts.map +1 -1
  116. package/server.d.ts.map +1 -1
  117. package/server.js +8 -4
  118. package/server.js.map +1 -1
  119. package/server.mjs +8 -4
  120. package/server.mjs.map +1 -1
  121. package/src/docs-search-tool.ts +48 -0
  122. package/src/handlers/get-accounts.ts +28 -0
  123. package/src/handlers/get-chat.ts +18 -0
  124. package/src/handlers/index.ts +29 -0
  125. package/src/handlers/list-chats.ts +47 -0
  126. package/src/handlers/list-messages.ts +33 -0
  127. package/src/handlers/open-app.ts +20 -0
  128. package/src/handlers/search-chats.ts +39 -0
  129. package/src/handlers/search-messages.ts +8 -0
  130. package/src/handlers/search.ts +24 -0
  131. package/src/handlers/send-message.ts +17 -0
  132. package/src/handlers/utils.ts +381 -0
  133. package/src/http.ts +3 -3
  134. package/src/options.ts +17 -7
  135. package/src/server.ts +8 -5
  136. package/src/tools/accounts/get-accounts.ts +3 -3
  137. package/src/tools/chats/archive-chat.ts +6 -6
  138. package/src/tools/chats/get-chat.ts +6 -7
  139. package/src/tools/chats/reminders/clear-chat-reminder.ts +10 -8
  140. package/src/tools/chats/reminders/set-chat-reminder.ts +6 -5
  141. package/src/tools/chats/search-chats.ts +5 -5
  142. package/src/tools/index.ts +8 -5
  143. package/src/tools/messages/list-messages.ts +51 -0
  144. package/src/tools/messages/search-messages.ts +6 -7
  145. package/src/tools/messages/send-message.ts +5 -5
  146. package/src/tools/{app/open-in-app.ts → top-level/focus-app.ts} +6 -6
  147. package/src/tools/{app → top-level}/search.ts +3 -3
  148. package/tools/accounts/get-accounts.js +3 -3
  149. package/tools/accounts/get-accounts.js.map +1 -1
  150. package/tools/accounts/get-accounts.mjs +3 -3
  151. package/tools/accounts/get-accounts.mjs.map +1 -1
  152. package/tools/chats/archive-chat.d.mts.map +1 -1
  153. package/tools/chats/archive-chat.d.ts.map +1 -1
  154. package/tools/chats/archive-chat.js +6 -5
  155. package/tools/chats/archive-chat.js.map +1 -1
  156. package/tools/chats/archive-chat.mjs +6 -5
  157. package/tools/chats/archive-chat.mjs.map +1 -1
  158. package/tools/chats/get-chat.d.mts.map +1 -1
  159. package/tools/chats/get-chat.d.ts.map +1 -1
  160. package/tools/chats/get-chat.js +6 -6
  161. package/tools/chats/get-chat.js.map +1 -1
  162. package/tools/chats/get-chat.mjs +6 -6
  163. package/tools/chats/get-chat.mjs.map +1 -1
  164. package/tools/chats/reminders/clear-chat-reminder.d.mts.map +1 -1
  165. package/tools/chats/reminders/clear-chat-reminder.d.ts.map +1 -1
  166. package/tools/chats/reminders/clear-chat-reminder.js +10 -7
  167. package/tools/chats/reminders/clear-chat-reminder.js.map +1 -1
  168. package/tools/chats/reminders/clear-chat-reminder.mjs +10 -7
  169. package/tools/chats/reminders/clear-chat-reminder.mjs.map +1 -1
  170. package/tools/chats/reminders/set-chat-reminder.d.mts.map +1 -1
  171. package/tools/chats/reminders/set-chat-reminder.d.ts.map +1 -1
  172. package/tools/chats/reminders/set-chat-reminder.js +6 -5
  173. package/tools/chats/reminders/set-chat-reminder.js.map +1 -1
  174. package/tools/chats/reminders/set-chat-reminder.mjs +6 -5
  175. package/tools/chats/reminders/set-chat-reminder.mjs.map +1 -1
  176. package/tools/chats/search-chats.js +5 -5
  177. package/tools/chats/search-chats.js.map +1 -1
  178. package/tools/chats/search-chats.mjs +5 -5
  179. package/tools/chats/search-chats.mjs.map +1 -1
  180. package/tools/index.d.mts.map +1 -1
  181. package/tools/index.d.ts.map +1 -1
  182. package/tools/index.js +8 -5
  183. package/tools/index.js.map +1 -1
  184. package/tools/index.mjs +8 -5
  185. package/tools/index.mjs.map +1 -1
  186. package/tools/messages/list-messages.d.mts +45 -0
  187. package/tools/messages/list-messages.d.mts.map +1 -0
  188. package/tools/messages/list-messages.d.ts +45 -0
  189. package/tools/messages/list-messages.d.ts.map +1 -0
  190. package/tools/messages/list-messages.js +47 -0
  191. package/tools/messages/list-messages.js.map +1 -0
  192. package/tools/messages/list-messages.mjs +43 -0
  193. package/tools/messages/list-messages.mjs.map +1 -0
  194. package/tools/messages/search-messages.d.mts.map +1 -1
  195. package/tools/messages/search-messages.d.ts.map +1 -1
  196. package/tools/messages/search-messages.js +6 -6
  197. package/tools/messages/search-messages.js.map +1 -1
  198. package/tools/messages/search-messages.mjs +6 -6
  199. package/tools/messages/search-messages.mjs.map +1 -1
  200. package/tools/messages/send-message.js +5 -5
  201. package/tools/messages/send-message.js.map +1 -1
  202. package/tools/messages/send-message.mjs +5 -5
  203. package/tools/messages/send-message.mjs.map +1 -1
  204. package/tools/{app/open-in-app.d.ts → top-level/focus-app.d.mts} +1 -1
  205. package/tools/top-level/focus-app.d.mts.map +1 -0
  206. package/tools/{app/open-in-app.d.mts → top-level/focus-app.d.ts} +1 -1
  207. package/tools/top-level/focus-app.d.ts.map +1 -0
  208. package/tools/{app/open-in-app.js → top-level/focus-app.js} +7 -7
  209. package/tools/top-level/focus-app.js.map +1 -0
  210. package/tools/{app/open-in-app.mjs → top-level/focus-app.mjs} +7 -7
  211. package/tools/top-level/focus-app.mjs.map +1 -0
  212. package/tools/top-level/search.d.mts.map +1 -0
  213. package/tools/top-level/search.d.ts.map +1 -0
  214. package/tools/{app → top-level}/search.js +3 -3
  215. package/tools/top-level/search.js.map +1 -0
  216. package/tools/{app → top-level}/search.mjs +3 -3
  217. package/tools/top-level/search.mjs.map +1 -0
  218. package/tools/app/open-in-app.d.mts.map +0 -1
  219. package/tools/app/open-in-app.d.ts.map +0 -1
  220. package/tools/app/open-in-app.js.map +0 -1
  221. package/tools/app/open-in-app.mjs.map +0 -1
  222. package/tools/app/search.d.mts.map +0 -1
  223. package/tools/app/search.d.ts.map +0 -1
  224. package/tools/app/search.js.map +0 -1
  225. package/tools/app/search.mjs.map +0 -1
  226. /package/tools/{app → top-level}/search.d.mts +0 -0
  227. /package/tools/{app → top-level}/search.d.ts +0 -0
@@ -0,0 +1,381 @@
1
+ import {
2
+ differenceInMilliseconds,
3
+ differenceInDays,
4
+ endOfDay,
5
+ format,
6
+ isToday,
7
+ isYesterday,
8
+ isSameYear,
9
+ parse,
10
+ } from 'date-fns';
11
+ import type * as Shared from '@beeper/desktop-api/resources/shared';
12
+ import type * as ChatsAPI from '@beeper/desktop-api/resources/chats/chats';
13
+ import { ToolCallResult } from '../tools/types';
14
+
15
+ const MILLIS_IN_WEEK = 86400000 * 7;
16
+
17
+ type Message = Shared.Message;
18
+ type Chat = ChatsAPI.Chat;
19
+ type User = Shared.User;
20
+ type MessageReaction = NonNullable<Message['reactions']>[number];
21
+
22
+ export const formatRelativeDate = (date: Date) => {
23
+ const timeDifference = differenceInMilliseconds(endOfDay(new Date()), date);
24
+
25
+ if (isToday(date)) return 'Today';
26
+ if (isYesterday(date)) return 'Yesterday';
27
+
28
+ return new Intl.DateTimeFormat(
29
+ 'default',
30
+ timeDifference < MILLIS_IN_WEEK ?
31
+ {
32
+ weekday: 'long',
33
+ }
34
+ : isSameYear(date, new Date()) ?
35
+ {
36
+ weekday: 'short',
37
+ month: 'short',
38
+ day: 'numeric',
39
+ }
40
+ : {
41
+ year: 'numeric',
42
+ month: 'short',
43
+ day: 'numeric',
44
+ },
45
+ ).format(date);
46
+ };
47
+
48
+ export const formatBytes = (bytes: number, decimals = 0) => {
49
+ if (bytes === 0) return '0 B';
50
+
51
+ const k = 1024;
52
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
53
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
54
+
55
+ return `${Math.floor(parseFloat((bytes / k ** i).toFixed(decimals)))}${sizes[i]}`;
56
+ };
57
+
58
+ const skinToneRegex = /\uD83C[\uDFFB-\uDFFF]/g;
59
+ const removeSkinTone = (emojiString: string): string => emojiString.replace(skinToneRegex, '');
60
+
61
+ export function groupReactions(reactions: MessageReaction[]): { [key: string]: MessageReaction[] } {
62
+ const map: { [key: string]: MessageReaction[] } = {};
63
+ reactions.forEach((reaction) => {
64
+ if (!reaction.reactionKey) return;
65
+ const key = removeSkinTone(reaction.reactionKey);
66
+ map[key] ||= [];
67
+ map[key].push(reaction);
68
+ });
69
+ return map;
70
+ }
71
+
72
+ export const getParticipantName = (participant: User, preferFirstName?: boolean): string =>
73
+ participant.fullName && preferFirstName ?
74
+ participant.fullName.split(' ')[0]!
75
+ : participant.fullName ||
76
+ participant.username ||
77
+ participant.email ||
78
+ participant.phoneNumber ||
79
+ participant.id;
80
+
81
+ export const createOpenLink = (baseURL: string, localChatIDOrChatID: string, messageKey?: string) =>
82
+ `${baseURL}/open/${encodeURIComponent(localChatIDOrChatID)}${messageKey ? `/${messageKey}` : ''}`;
83
+
84
+ export const formatParticipantsToMarkdown = (participants: User[] | undefined, limit = 3): string => {
85
+ if (!participants || participants.length === 0) return '';
86
+
87
+ const names = participants
88
+ .slice(0, limit)
89
+ .map((p) => p.fullName || p.username || p.id)
90
+ .filter(Boolean);
91
+
92
+ if (participants.length > limit) {
93
+ const othersCount = participants.length - limit;
94
+ names.push(`& ${othersCount} other${othersCount === 1 ? '' : 's'}`);
95
+ }
96
+
97
+ return names.join(', ');
98
+ };
99
+
100
+ export const formatReactionsToMarkdown = (
101
+ reactions: Message['reactions'],
102
+ participants?: Map<string, User>,
103
+ ): string => {
104
+ if (!reactions || reactions.length === 0) return '';
105
+
106
+ const reactionMap = groupReactions(reactions);
107
+ const reactionParts: string[] = [];
108
+ for (const [reactionKey, reactionList] of Object.entries(reactionMap)) {
109
+ const count = reactionList.length;
110
+ const reactorNames = reactionList
111
+ .slice(0, 5)
112
+ .map((r) => {
113
+ if (!r.participantID) return null;
114
+ const participant = participants?.get(r.participantID);
115
+ return participant ? getParticipantName(participant) : r.participantID;
116
+ })
117
+ .filter(Boolean);
118
+
119
+ let reactorInfo = '';
120
+ if (reactorNames.length > 0) {
121
+ if (count > 5) {
122
+ const othersCount = count - 5;
123
+ reactorInfo = ` (${reactorNames.join(', ')} & ${othersCount} other${othersCount === 1 ? '' : 's'})`;
124
+ } else {
125
+ reactorInfo = ` (${reactorNames.join(', ')})`;
126
+ }
127
+ }
128
+
129
+ reactionParts.push(`${reactionKey} ${count}${reactorInfo}`);
130
+ }
131
+
132
+ return reactionParts.length > 0 ? ` [${reactionParts.join(' ')}]` : '';
133
+ };
134
+
135
+ export const formatAttachmentToMarkdown = (attachment: Shared.Attachment | undefined): string => {
136
+ if (!attachment) return '';
137
+
138
+ const typeEmoji =
139
+ {
140
+ img: '🖼',
141
+ video: '🎥',
142
+ audio: '🎵',
143
+ unknown: '📎',
144
+ }[attachment.type] || '📎';
145
+
146
+ const fileName = attachment.fileName || 'file';
147
+ const url = attachment.srcURL || '';
148
+ const hasBothDimensions =
149
+ typeof attachment.size?.width === 'number' && typeof attachment.size?.height === 'number';
150
+
151
+ const metaInfo: string[] = [];
152
+ if (attachment.fileSize) {
153
+ metaInfo.push(formatBytes(attachment.fileSize));
154
+ }
155
+ if (hasBothDimensions) {
156
+ metaInfo.push(`${attachment.size!.width}x${attachment.size!.height}`);
157
+ }
158
+
159
+ const metaString = metaInfo.length > 0 ? ` (${metaInfo.join(', ')})` : '';
160
+
161
+ return `\n${typeEmoji} [${fileName}](${url})${metaString}`;
162
+ };
163
+
164
+ export const formatChatToMarkdown = (chat: Chat, baseURL: string | undefined): string => {
165
+ const openURL = baseURL ? createOpenLink(baseURL, chat.localChatID ?? chat.id) : undefined;
166
+ const title = openURL ? `[${chat.title}](${openURL})` : chat.title;
167
+ const participantList =
168
+ chat.participants?.items ? formatParticipantsToMarkdown(chat.participants.items, 3) : '';
169
+ const participantInfo = participantList ? ` with ${participantList}` : '';
170
+ const lines: string[] = [];
171
+ lines.push(`\n## ${title} (chatID: ${chat.localChatID})`);
172
+ let chatLine = `Chat on ${chat.network}${participantInfo}.`;
173
+ if (typeof chat.unreadCount === 'number' && chat.unreadCount > 0) {
174
+ chatLine += ` It has ${chat.unreadCount} unread message${chat.unreadCount === 1 ? '' : 's'}.`;
175
+ }
176
+ lines.push(chatLine);
177
+ lines.push(`**Type**: ${chat.type}`);
178
+ if (chat.lastActivity) lines.push(`**Last Activity**: ${chat.lastActivity}`);
179
+ const status: string[] = [];
180
+ if (chat.isArchived) status.push('archived');
181
+ if (chat.isMuted) status.push('muted');
182
+ if (chat.isPinned) status.push('pinned');
183
+ if (status.length > 0) lines.push(`This chat is ${status.join(', ')}.`);
184
+ return lines.join('\n');
185
+ };
186
+
187
+ const parseLocalDateKey = (key: string): Date => {
188
+ const parsed = parse(key, 'yyyy-MM-dd', new Date());
189
+ if (isNaN(parsed.getTime())) {
190
+ throw new Error(`Invalid date key: ${key}`);
191
+ }
192
+ return parsed;
193
+ };
194
+
195
+ interface MessagesResponse {
196
+ items: Message[];
197
+ chats: Record<string, Chat>;
198
+ hasMore?: boolean;
199
+ oldestCursor?: string;
200
+ newestCursor?: string;
201
+ }
202
+
203
+ export const mapMessagesToText = (
204
+ output: Shared.MessagesCursorSearch,
205
+ input?: {
206
+ query?: string;
207
+ sender?: string;
208
+ mediaTypes?: string[];
209
+ },
210
+ ctx?: { apiBaseURL?: string; maxTextLength?: number },
211
+ ) => {
212
+ const { items, hasMore } = output;
213
+ const chats = (output as any)?.body?.chats ?? {};
214
+
215
+ const messageCount = items.length;
216
+ const chatCount = Object.keys(chats).length;
217
+
218
+ // Determine if search filters would cause gaps in timeline
219
+ // Gaps occur when filtering by: query text, sender, or media types
220
+ // Gaps do NOT occur when only filtering by: chatIDs, accountIDs, chatType, or date ranges
221
+ const hasGapCausingFilters =
222
+ input && (input.query || input.sender || (input.mediaTypes && input.mediaTypes.length > 0));
223
+
224
+ const paginationInfo: string[] = [];
225
+ if (messageCount === 0) {
226
+ if (!chats || chatCount === 0) {
227
+ paginationInfo.push('No matching chats found');
228
+ } else {
229
+ paginationInfo.push(`No messages found in ${chatCount} chat${chatCount === 1 ? '' : 's'}`);
230
+ }
231
+ } else if (hasMore) {
232
+ paginationInfo.push(
233
+ `Found ${messageCount}+ messages across ${chatCount} chat${
234
+ chatCount === 1 ? '' : 's'
235
+ } (showing ${messageCount})`,
236
+ );
237
+ if (output.oldestCursor) {
238
+ paginationInfo.push(`Next page (older): cursor='${output.oldestCursor}', direction='before'`);
239
+ }
240
+ if (output.newestCursor) {
241
+ paginationInfo.push(`Previous page (newer): cursor='${output.newestCursor}', direction='after'`);
242
+ }
243
+ } else {
244
+ paginationInfo.push(
245
+ `Found ${messageCount} message${messageCount === 1 ? '' : 's'} across ${chatCount} chat${
246
+ chatCount === 1 ? '' : 's'
247
+ } (complete)`,
248
+ );
249
+ }
250
+
251
+ if (hasGapCausingFilters && messageCount > 0) {
252
+ paginationInfo.push('⚠️ Filtered results: only showing messages matching your search criteria.');
253
+ }
254
+
255
+ const messagesByChat = new Map<string, typeof items>();
256
+ for (const message of items) {
257
+ const chatMessages = messagesByChat.get(message.chatID) || [];
258
+ chatMessages.push(message);
259
+ messagesByChat.set(message.chatID, chatMessages);
260
+ }
261
+
262
+ const chatSummaries: string[] = [];
263
+ for (const [chatID, messages] of messagesByChat) {
264
+ const chat = chats[chatID];
265
+ if (chat) {
266
+ chatSummaries.push(`# ${chat.title} [${messages.length} message${messages.length === 1 ? '' : 's'}]`);
267
+ }
268
+ }
269
+
270
+ const headerLines = [...paginationInfo, '', ...chatSummaries];
271
+
272
+ const summary = headerLines.join('\n');
273
+
274
+ const chatSections: string[] = [];
275
+
276
+ for (const [chatID, messages] of messagesByChat) {
277
+ const chat = chats[chatID];
278
+ if (!chat) continue;
279
+
280
+ const participantList =
281
+ chat.participants?.items ? formatParticipantsToMarkdown(chat.participants.items, 3) : '';
282
+ const participantInfo = participantList ? ` with ${participantList}` : '';
283
+ const openURL = ctx?.apiBaseURL ? createOpenLink(ctx.apiBaseURL, chat.localChatID ?? chat.id) : undefined;
284
+ const title = openURL ? `[${chat.title}](${openURL})` : chat.title;
285
+ chatSections.push(`# ${title} (chatID: ${chat.localChatID})`);
286
+ chatSections.push(`Chat on ${chat.network}${participantInfo}.`);
287
+ chatSections.push('');
288
+
289
+ const messagesByDate = new Map<string, Message[]>();
290
+ for (const message of messages) {
291
+ const dateKey = format(new Date(message.timestamp), 'yyyy-MM-dd');
292
+ const dateMessages = messagesByDate.get(dateKey) || [];
293
+ dateMessages.push(message);
294
+ messagesByDate.set(dateKey, dateMessages);
295
+ }
296
+
297
+ const sortedDates = Array.from(messagesByDate.keys()).sort();
298
+
299
+ const participantMap =
300
+ chat?.participants?.items ?
301
+ new Map<string, User>(chat.participants.items.map((p: User) => [p.id, p]))
302
+ : undefined;
303
+
304
+ for (let i = 0; i < sortedDates.length; i++) {
305
+ const dateKey = sortedDates[i]!;
306
+ const dateObj = parseLocalDateKey(dateKey);
307
+ const relativeTime = formatRelativeDate(dateObj);
308
+ chatSections.push(`## ${relativeTime} (${dateKey})`);
309
+ chatSections.push('');
310
+
311
+ const dateMessages = messagesByDate.get(dateKey) || [];
312
+ dateMessages.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
313
+
314
+ for (const message of dateMessages) {
315
+ const time = new Date(message.timestamp);
316
+ const timeStr = format(time, 'HH:mm');
317
+
318
+ const baseSenderName = message.senderName || message.senderID;
319
+ const senderName = message.isSender ? `${baseSenderName} (You)` : baseSenderName;
320
+
321
+ const maxTextLength = ctx?.maxTextLength ?? 1000;
322
+ let text = message.text || '';
323
+ if (text && text.length > maxTextLength) {
324
+ const remainingChars = text.length - maxTextLength;
325
+ text = text.substring(0, maxTextLength) + `... [+${remainingChars} chars]`;
326
+ }
327
+
328
+ const attachment = message.attachments?.[0]; // Assume single attachment
329
+ const attachmentChatID = chat.localChatID ?? chat.id;
330
+ const attachmentLink =
331
+ attachment && attachmentChatID ?
332
+ `\n📎 [${attachment.fileName || 'attachment'}](beeper-mcp://attachments/${attachmentChatID}/${
333
+ message.id
334
+ }/0)`
335
+ : '';
336
+ const reactionsStr = formatReactionsToMarkdown(message.reactions, participantMap);
337
+
338
+ const sortKeyLink =
339
+ chat.localChatID ?
340
+ `([open at sort key](${createOpenLink(
341
+ ctx?.apiBaseURL || '',
342
+ chat.localChatID,
343
+ String(message.sortKey),
344
+ )}))`
345
+ : `(sortKey: ${message.sortKey})`;
346
+ const messageStr = `**${senderName}** (${timeStr}): ${text}${attachmentLink}${reactionsStr} ${sortKeyLink}`;
347
+
348
+ chatSections.push(messageStr);
349
+ chatSections.push('');
350
+ }
351
+
352
+ // Add date gap indicator when dates are not consecutive
353
+ if (i < sortedDates.length - 1) {
354
+ const nextDateKey = sortedDates[i + 1]!;
355
+ const currentDate = parseLocalDateKey(dateKey!);
356
+ const nextDate = parseLocalDateKey(nextDateKey);
357
+ const dayDiff = differenceInDays(nextDate, currentDate);
358
+
359
+ // Only show gap if dates are not consecutive
360
+ if (dayDiff > 1) {
361
+ const gapDays = dayDiff - 1;
362
+ chatSections.push(`*[... ${gapDays} day${gapDays === 1 ? '' : 's'} gap ...]*`);
363
+ chatSections.push('');
364
+ }
365
+ }
366
+ }
367
+ }
368
+
369
+ return [summary, '', ...chatSections].join('\n');
370
+ };
371
+
372
+ export function asMarkdownContentResult(text: string | string[]): ToolCallResult {
373
+ return {
374
+ content: [
375
+ {
376
+ type: 'text',
377
+ text: Array.isArray(text) ? text.join('\n') : text,
378
+ },
379
+ ],
380
+ };
381
+ }
package/src/http.ts CHANGED
@@ -71,7 +71,7 @@ const newServer = ({
71
71
  return server;
72
72
  };
73
73
 
74
- const post =
74
+ export const post =
75
75
  (options: { clientOptions: ClientOptions; mcpOptions: McpOptions }) =>
76
76
  async (req: express.Request, res: express.Response) => {
77
77
  const server = newServer({ ...options, req, res });
@@ -85,7 +85,7 @@ const post =
85
85
  await transport.handleRequest(req, res, req.body);
86
86
  };
87
87
 
88
- const get = async (req: express.Request, res: express.Response) => {
88
+ export const get = async (req: express.Request, res: express.Response) => {
89
89
  res.status(405).json({
90
90
  jsonrpc: '2.0',
91
91
  error: {
@@ -95,7 +95,7 @@ const get = async (req: express.Request, res: express.Response) => {
95
95
  });
96
96
  };
97
97
 
98
- const del = async (req: express.Request, res: express.Response) => {
98
+ export const del = async (req: express.Request, res: express.Response) => {
99
99
  res.status(405).json({
100
100
  jsonrpc: '2.0',
101
101
  error: {
package/src/options.ts CHANGED
@@ -17,6 +17,7 @@ export type McpOptions = {
17
17
  includeDynamicTools?: boolean | undefined;
18
18
  includeAllTools?: boolean | undefined;
19
19
  includeCodeTools?: boolean | undefined;
20
+ includeDocsTools?: boolean | undefined;
20
21
  filters?: Filter[] | undefined;
21
22
  capabilities?: Partial<ClientCapabilities> | undefined;
22
23
  };
@@ -55,13 +56,13 @@ export function parseCLIOptions(): CLIOptions {
55
56
  .option('tools', {
56
57
  type: 'string',
57
58
  array: true,
58
- choices: ['dynamic', 'all', 'code'],
59
+ choices: ['dynamic', 'all', 'code', 'docs'],
59
60
  description: 'Use dynamic tools or all tools',
60
61
  })
61
62
  .option('no-tools', {
62
63
  type: 'string',
63
64
  array: true,
64
- choices: ['dynamic', 'all', 'code'],
65
+ choices: ['dynamic', 'all', 'code', 'docs'],
65
66
  description: 'Do not use any dynamic or all tools',
66
67
  })
67
68
  .option('tool', {
@@ -245,13 +246,15 @@ export function parseCLIOptions(): CLIOptions {
245
246
  }
246
247
  }
247
248
 
248
- const shouldIncludeToolType = (toolType: 'dynamic' | 'all' | 'code') =>
249
- explicitTools ? argv.tools?.includes(toolType) && !argv.noTools?.includes(toolType) : undefined;
249
+ const shouldIncludeToolType = (toolType: 'dynamic' | 'all' | 'code' | 'docs') =>
250
+ argv.noTools?.includes(toolType) ? false
251
+ : argv.tools?.includes(toolType) ? true
252
+ : undefined;
250
253
 
251
- const explicitTools = Boolean(argv.tools || argv.noTools);
252
254
  const includeDynamicTools = shouldIncludeToolType('dynamic');
253
255
  const includeAllTools = shouldIncludeToolType('all');
254
256
  const includeCodeTools = shouldIncludeToolType('code');
257
+ const includeDocsTools = shouldIncludeToolType('docs');
255
258
 
256
259
  const transport = argv.transport as 'stdio' | 'http';
257
260
 
@@ -261,6 +264,7 @@ export function parseCLIOptions(): CLIOptions {
261
264
  includeDynamicTools,
262
265
  includeAllTools,
263
266
  includeCodeTools,
267
+ includeDocsTools,
264
268
  filters,
265
269
  capabilities: clientCapabilities,
266
270
  list: argv.list || false,
@@ -280,8 +284,8 @@ const coerceArray = <T extends z.ZodTypeAny>(zodType: T) =>
280
284
  );
281
285
 
282
286
  const QueryOptions = z.object({
283
- tools: coerceArray(z.enum(['dynamic', 'all'])).describe('Use dynamic tools or all tools'),
284
- no_tools: coerceArray(z.enum(['dynamic', 'all'])).describe('Do not use dynamic tools or all tools'),
287
+ tools: coerceArray(z.enum(['dynamic', 'all', 'docs'])).describe('Use dynamic tools or all tools'),
288
+ no_tools: coerceArray(z.enum(['dynamic', 'all', 'docs'])).describe('Do not use dynamic tools or all tools'),
285
289
  tool: coerceArray(z.string()).describe('Include tools matching the specified names'),
286
290
  resource: coerceArray(z.string()).describe('Include tools matching the specified resources'),
287
291
  operation: coerceArray(z.enum(['read', 'write'])).describe(
@@ -376,11 +380,17 @@ export function parseQueryOptions(defaultOptions: McpOptions, query: unknown): M
376
380
  : queryOptions.tools?.includes('all') ? true
377
381
  : defaultOptions.includeAllTools;
378
382
 
383
+ let docsTools: boolean | undefined =
384
+ queryOptions.no_tools && queryOptions.no_tools?.includes('docs') ? false
385
+ : queryOptions.tools?.includes('docs') ? true
386
+ : defaultOptions.includeDocsTools;
387
+
379
388
  return {
380
389
  client: queryOptions.client ?? defaultOptions.client,
381
390
  includeDynamicTools: dynamicTools,
382
391
  includeAllTools: allTools,
383
392
  includeCodeTools: undefined,
393
+ includeDocsTools: docsTools,
384
394
  filters,
385
395
  capabilities: clientCapabilities,
386
396
  };
package/src/server.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  } from './compat';
22
22
  import { dynamicTools } from './dynamic-tools';
23
23
  import { codeTool } from './code-tool';
24
+ import docsSearchTool from './docs-search-tool';
24
25
  import { McpOptions } from './options';
25
26
 
26
27
  export { McpOptions } from './options';
@@ -33,7 +34,7 @@ export const newMcpServer = () =>
33
34
  new McpServer(
34
35
  {
35
36
  name: 'beeper_desktop_api_api',
36
- version: '0.1.5',
37
+ version: '4.2.1',
37
38
  },
38
39
  {
39
40
  capabilities: { tools: {}, logging: {} },
@@ -151,7 +152,7 @@ export function initMcpServer(params: {
151
152
  export async function selectTools(endpoints: Endpoint[], options?: McpOptions): Promise<Endpoint[]> {
152
153
  const filteredEndpoints = query(options?.filters ?? [], endpoints);
153
154
 
154
- let includedTools = filteredEndpoints;
155
+ let includedTools = filteredEndpoints.slice();
155
156
 
156
157
  if (includedTools.length > 0) {
157
158
  if (options?.includeDynamicTools) {
@@ -159,16 +160,18 @@ export async function selectTools(endpoints: Endpoint[], options?: McpOptions):
159
160
  }
160
161
  } else {
161
162
  if (options?.includeAllTools) {
162
- includedTools = endpoints;
163
+ includedTools = endpoints.slice();
163
164
  } else if (options?.includeDynamicTools) {
164
165
  includedTools = dynamicTools(endpoints);
165
166
  } else if (options?.includeCodeTools) {
166
167
  includedTools = [await codeTool()];
167
168
  } else {
168
- includedTools = endpoints;
169
+ includedTools = endpoints.slice();
169
170
  }
170
171
  }
171
-
172
+ if (options?.includeDocsTools ?? true) {
173
+ includedTools.push(docsSearchTool);
174
+ }
172
175
  const capabilities = { ...defaultClientCapabilities, ...options?.capabilities };
173
176
  return applyCompatibilityTransformations(includedTools, capabilities);
174
177
  }
@@ -10,13 +10,13 @@ export const metadata: Metadata = {
10
10
  operation: 'read',
11
11
  tags: ['accounts'],
12
12
  httpMethod: 'get',
13
- httpPath: '/v0/get-accounts',
14
- operationId: 'get_accounts',
13
+ httpPath: '/v1/accounts',
14
+ operationId: 'getAccounts',
15
15
  };
16
16
 
17
17
  export const tool: Tool = {
18
18
  name: 'get_accounts',
19
- description: 'List connected accounts on this device. Use to pick account context.',
19
+ description: 'List connected accounts on this device.',
20
20
  inputSchema: {
21
21
  type: 'object',
22
22
  properties: {},
@@ -10,8 +10,8 @@ export const metadata: Metadata = {
10
10
  operation: 'write',
11
11
  tags: ['chats'],
12
12
  httpMethod: 'post',
13
- httpPath: '/v0/archive-chat',
14
- operationId: 'archive_chat',
13
+ httpPath: '/v1/chats/{chatID}/archive',
14
+ operationId: 'archiveChat',
15
15
  };
16
16
 
17
17
  export const tool: Tool = {
@@ -22,8 +22,7 @@ export const tool: Tool = {
22
22
  properties: {
23
23
  chatID: {
24
24
  type: 'string',
25
- description:
26
- 'The identifier of the chat to archive or unarchive (accepts both chatID and local chat ID)',
25
+ description: 'Unique identifier of the chat.',
27
26
  },
28
27
  archived: {
29
28
  type: 'boolean',
@@ -36,8 +35,9 @@ export const tool: Tool = {
36
35
  };
37
36
 
38
37
  export const handler = async (client: BeeperDesktop, args: Record<string, unknown> | undefined) => {
39
- const body = args as any;
40
- return asTextContentResult(await client.chats.archive(body));
38
+ const { chatID, ...body } = args as any;
39
+ const response = await client.chats.archive(chatID, body).asResponse();
40
+ return asTextContentResult(await response.text());
41
41
  };
42
42
 
43
43
  export default { metadata, tool, handler };
@@ -10,8 +10,8 @@ export const metadata: Metadata = {
10
10
  operation: 'read',
11
11
  tags: ['chats'],
12
12
  httpMethod: 'get',
13
- httpPath: '/v0/get-chat',
14
- operationId: 'get_chat',
13
+ httpPath: '/v1/chats/{chatID}',
14
+ operationId: 'getChat',
15
15
  };
16
16
 
17
17
  export const tool: Tool = {
@@ -22,13 +22,12 @@ export const tool: Tool = {
22
22
  properties: {
23
23
  chatID: {
24
24
  type: 'string',
25
- description:
26
- "Unique identifier of the chat to retrieve. Not available for iMessage chats. Participants are limited by 'maxParticipantCount'.",
25
+ description: 'Unique identifier of the chat.',
27
26
  },
28
27
  maxParticipantCount: {
29
28
  type: 'integer',
30
29
  description:
31
- 'Maximum number of participants to return. Use -1 for all; otherwise 0–500. Defaults to 20.',
30
+ 'Maximum number of participants to return. Use -1 for all; otherwise 0–500. Defaults to all (-1).',
32
31
  },
33
32
  },
34
33
  required: ['chatID'],
@@ -39,8 +38,8 @@ export const tool: Tool = {
39
38
  };
40
39
 
41
40
  export const handler = async (client: BeeperDesktop, args: Record<string, unknown> | undefined) => {
42
- const body = args as any;
43
- return asTextContentResult(await client.chats.retrieve(body));
41
+ const { chatID, ...body } = args as any;
42
+ return asTextContentResult(await client.chats.retrieve(chatID, body));
44
43
  };
45
44
 
46
45
  export default { metadata, tool, handler };
@@ -9,9 +9,9 @@ export const metadata: Metadata = {
9
9
  resource: 'chats.reminders',
10
10
  operation: 'write',
11
11
  tags: ['chats'],
12
- httpMethod: 'post',
13
- httpPath: '/v0/clear-chat-reminder',
14
- operationId: 'clear_chat_reminder',
12
+ httpMethod: 'delete',
13
+ httpPath: '/v1/chats/{chatID}/reminders',
14
+ operationId: 'clearChatReminder',
15
15
  };
16
16
 
17
17
  export const tool: Tool = {
@@ -22,18 +22,20 @@ export const tool: Tool = {
22
22
  properties: {
23
23
  chatID: {
24
24
  type: 'string',
25
- description:
26
- 'The identifier of the chat to clear reminder from (accepts both chatID and local chat ID)',
25
+ description: 'Unique identifier of the chat.',
27
26
  },
28
27
  },
29
28
  required: ['chatID'],
30
29
  },
31
- annotations: {},
30
+ annotations: {
31
+ idempotentHint: true,
32
+ },
32
33
  };
33
34
 
34
35
  export const handler = async (client: BeeperDesktop, args: Record<string, unknown> | undefined) => {
35
- const body = args as any;
36
- return asTextContentResult(await client.chats.reminders.delete(body));
36
+ const { chatID, ...body } = args as any;
37
+ const response = await client.chats.reminders.delete(chatID).asResponse();
38
+ return asTextContentResult(await response.text());
37
39
  };
38
40
 
39
41
  export default { metadata, tool, handler };