whats-mcp 0.1.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.
@@ -0,0 +1,157 @@
1
+ /**
2
+ * whats-mcp — Analytics tools (5 tools).
3
+ *
4
+ * analytics_overview, analytics_top_chats, analytics_chat_insights,
5
+ * analytics_timeline, analytics_search
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const { phoneToJid, okResult, errResult } = require("../helpers");
11
+
12
+ module.exports = [
13
+ {
14
+ definition: {
15
+ name: "analytics_overview",
16
+ description:
17
+ "Return a local analytics summary built from the cached WhatsApp store." +
18
+ " Includes totals, top chats, top tokens, top senders, and activity trends.",
19
+ inputSchema: {
20
+ type: "object",
21
+ properties: {
22
+ top_chats: { type: "integer", description: "Number of top chats to include. Default 10." },
23
+ top_tokens: { type: "integer", description: "Number of top tokens to include. Default 20." },
24
+ top_senders: { type: "integer", description: "Number of top senders to include. Default 10." },
25
+ days: { type: "integer", description: "Number of daily activity buckets to include. Default 30." },
26
+ },
27
+ },
28
+ },
29
+ handler: async (args, { store }) => okResult(store.getAnalyticsOverview(args)),
30
+ },
31
+
32
+ {
33
+ definition: {
34
+ name: "analytics_top_chats",
35
+ description:
36
+ "Rank chats using the local analytics index." +
37
+ " Can sort by message count, last activity, active days, or participant count.",
38
+ inputSchema: {
39
+ type: "object",
40
+ properties: {
41
+ limit: { type: "integer", description: "Maximum number of chats to return. Default 20." },
42
+ sort_by: {
43
+ type: "string",
44
+ enum: ["message_count", "last_activity", "active_days", "participants"],
45
+ description: "Sort criterion. Default message_count.",
46
+ },
47
+ },
48
+ },
49
+ },
50
+ handler: async ({ limit, sort_by }, { store }) => okResult({
51
+ count: Math.min(limit || 20, 200),
52
+ chats: store.listAnalyticsTopChats({ limit, sort_by }),
53
+ }),
54
+ },
55
+
56
+ {
57
+ definition: {
58
+ name: "analytics_chat_insights",
59
+ description:
60
+ "Return detailed local analytics for one chat, including top tokens, senders, activity, and recent messages.",
61
+ inputSchema: {
62
+ type: "object",
63
+ properties: {
64
+ jid: { type: "string", description: "Chat JID or phone number." },
65
+ top_tokens: { type: "integer", description: "Maximum number of top tokens to include. Default 15." },
66
+ top_senders: { type: "integer", description: "Maximum number of top senders to include. Default 10." },
67
+ days: { type: "integer", description: "Number of daily activity buckets to include. Default 30." },
68
+ recent_messages: { type: "integer", description: "Number of recent messages to include. Default 5." },
69
+ },
70
+ required: ["jid"],
71
+ },
72
+ },
73
+ handler: async ({ jid, ...options }, { store }) => {
74
+ const chatJid = phoneToJid(jid);
75
+ const result = store.getChatAnalytics(chatJid, options)
76
+ || store.getChatAnalytics(jid, options);
77
+ if (!result) {
78
+ return errResult(`No analytics available for chat ${jid}.`);
79
+ }
80
+ return okResult(result);
81
+ },
82
+ },
83
+
84
+ {
85
+ definition: {
86
+ name: "analytics_timeline",
87
+ description:
88
+ "Return a daily activity timeline from the local analytics index, globally or for one chat.",
89
+ inputSchema: {
90
+ type: "object",
91
+ properties: {
92
+ jid: { type: "string", description: "Optional chat JID or phone number." },
93
+ days: { type: "integer", description: "Number of days to include. Default 30." },
94
+ },
95
+ },
96
+ },
97
+ handler: async ({ jid, days }, { store }) => {
98
+ const result = store.getActivityTimeline({
99
+ jid: jid ? phoneToJid(jid) : undefined,
100
+ days,
101
+ }) || (jid ? store.getActivityTimeline({ jid, days }) : null);
102
+ if (!result) {
103
+ return errResult(`No timeline available for chat ${jid}.`);
104
+ }
105
+ return okResult(result);
106
+ },
107
+ },
108
+
109
+ {
110
+ definition: {
111
+ name: "analytics_search",
112
+ description:
113
+ "Run a ranked search over the local analytics index using token matches, phrase matches, and recency." +
114
+ " Supports time range and multi-JID filtering.",
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ query: { type: "string", description: "Search query." },
119
+ jid: { type: "string", description: "Optional chat JID or phone number." },
120
+ jids: {
121
+ type: "array",
122
+ items: { type: "string" },
123
+ description: "Optional: search across multiple chat JIDs. Takes precedence over jid.",
124
+ },
125
+ limit: { type: "integer", description: "Maximum number of results. Default 20." },
126
+ since: {
127
+ type: "integer",
128
+ description: "Unix timestamp: only include messages at or after this time.",
129
+ },
130
+ until: {
131
+ type: "integer",
132
+ description: "Unix timestamp: only include messages at or before this time.",
133
+ },
134
+ },
135
+ required: ["query"],
136
+ },
137
+ },
138
+ handler: async ({ query, jid, jids, limit, since, until }, { store }) => {
139
+ let chatJids = undefined;
140
+ if (jids && jids.length > 0) {
141
+ chatJids = jids.map(phoneToJid);
142
+ } else if (jid) {
143
+ chatJids = phoneToJid(jid);
144
+ }
145
+ const opts = {
146
+ since: since || undefined,
147
+ until: until || undefined,
148
+ };
149
+ const messages = store.analyticsSearch(query, chatJids, limit, opts);
150
+ return okResult({
151
+ query,
152
+ count: messages.length,
153
+ messages,
154
+ });
155
+ },
156
+ },
157
+ ];
@@ -0,0 +1,215 @@
1
+ /**
2
+ * whats-mcp — Newsletter / Channel tools (5 tools).
3
+ *
4
+ * create_channel, get_channel_info, manage_channel,
5
+ * update_channel, delete_channel
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const {
11
+ newsletterJid, resolveMedia, okResult, errResult,
12
+ } = require("../helpers");
13
+
14
+ function _fmtChannel(meta) {
15
+ return {
16
+ jid: meta.id,
17
+ name: meta.name || meta.subject || null,
18
+ description: meta.description || meta.desc || null,
19
+ subscriber_count: meta.subscribers || meta.subscriberCount || null,
20
+ creation_time: meta.creation ? Number(meta.creation) : null,
21
+ picture_url: meta.picture || meta.pictureUrl || null,
22
+ invite_link: meta.inviteLink || null,
23
+ state: meta.state || null,
24
+ verification: meta.verification || null,
25
+ mute: meta.mute || null,
26
+ };
27
+ }
28
+
29
+ module.exports = [
30
+ // 1. create_channel
31
+ {
32
+ definition: {
33
+ name: "create_channel",
34
+ description:
35
+ "Create a new WhatsApp Channel (Newsletter)." +
36
+ " Returns the channel metadata including JID.",
37
+ inputSchema: {
38
+ type: "object",
39
+ properties: {
40
+ name: { type: "string", description: "Channel name." },
41
+ description: { type: "string", description: "Optional channel description." },
42
+ picture: { type: "string", description: "Optional profile picture: URL, base64, or file path." },
43
+ },
44
+ required: ["name"],
45
+ },
46
+ },
47
+ handler: async ({ name, description, picture }, { sock }) => {
48
+ const opts = { name };
49
+ if (description) opts.description = description;
50
+ if (picture) {
51
+ const media = resolveMedia(picture);
52
+ if (Buffer.isBuffer(media)) {
53
+ opts.picture = media;
54
+ } else if (media.url) {
55
+ const resp = await fetch(media.url);
56
+ opts.picture = Buffer.from(await resp.arrayBuffer());
57
+ }
58
+ }
59
+ const result = await sock.newsletterCreate(name, opts);
60
+ return okResult({
61
+ status: "created",
62
+ channel: _fmtChannel(result),
63
+ });
64
+ },
65
+ },
66
+
67
+ // 2. get_channel_info
68
+ {
69
+ definition: {
70
+ name: "get_channel_info",
71
+ description:
72
+ "Get metadata for a WhatsApp Channel (Newsletter)." +
73
+ " You can fetch by JID or invite link.",
74
+ inputSchema: {
75
+ type: "object",
76
+ properties: {
77
+ jid: {
78
+ type: "string",
79
+ description: "Channel JID (e.g. 120363xxx@newsletter) or invite link.",
80
+ },
81
+ },
82
+ required: ["jid"],
83
+ },
84
+ },
85
+ handler: async ({ jid }, { sock }) => {
86
+ let meta;
87
+ if (jid.startsWith("https://") || jid.startsWith("http://")) {
88
+ // Invite link -> extract code
89
+ const code = jid.split("/").pop();
90
+ meta = await sock.newsletterMetadata("invite", code);
91
+ } else {
92
+ const channelJid = newsletterJid(jid);
93
+ meta = await sock.newsletterMetadata("jid", channelJid);
94
+ }
95
+ return okResult({ channel: _fmtChannel(meta) });
96
+ },
97
+ },
98
+
99
+ // 3. manage_channel
100
+ {
101
+ definition: {
102
+ name: "manage_channel",
103
+ description:
104
+ "Follow (subscribe), unfollow, mute, or unmute a WhatsApp Channel.",
105
+ inputSchema: {
106
+ type: "object",
107
+ properties: {
108
+ jid: { type: "string", description: "Channel JID." },
109
+ action: {
110
+ type: "string",
111
+ enum: ["follow", "unfollow", "mute", "unmute"],
112
+ description: "Action to perform.",
113
+ },
114
+ },
115
+ required: ["jid", "action"],
116
+ },
117
+ },
118
+ handler: async ({ jid, action }, { sock }) => {
119
+ const channelJid = newsletterJid(jid);
120
+
121
+ if (action === "follow") {
122
+ await sock.newsletterFollow(channelJid);
123
+ return okResult({ status: "followed", jid: channelJid });
124
+ }
125
+ if (action === "unfollow") {
126
+ await sock.newsletterUnfollow(channelJid);
127
+ return okResult({ status: "unfollowed", jid: channelJid });
128
+ }
129
+ if (action === "mute") {
130
+ await sock.newsletterMute(channelJid);
131
+ return okResult({ status: "muted", jid: channelJid });
132
+ }
133
+ if (action === "unmute") {
134
+ await sock.newsletterUnmute(channelJid);
135
+ return okResult({ status: "unmuted", jid: channelJid });
136
+ }
137
+ return errResult(`Unknown action: ${action}`);
138
+ },
139
+ },
140
+
141
+ // 4. update_channel
142
+ {
143
+ definition: {
144
+ name: "update_channel",
145
+ description: "Update a channel's name, description, or picture.",
146
+ inputSchema: {
147
+ type: "object",
148
+ properties: {
149
+ jid: { type: "string", description: "Channel JID." },
150
+ name: { type: "string", description: "New channel name." },
151
+ description: { type: "string", description: "New channel description." },
152
+ picture: { type: "string", description: "New picture: URL, base64, or file path. Use 'remove' to delete." },
153
+ },
154
+ required: ["jid"],
155
+ },
156
+ },
157
+ handler: async ({ jid, name, description, picture }, { sock }) => {
158
+ const channelJid = newsletterJid(jid);
159
+ const updates = [];
160
+
161
+ if (name) {
162
+ await sock.newsletterUpdateName(channelJid, name);
163
+ updates.push("name");
164
+ }
165
+ if (description !== undefined) {
166
+ await sock.newsletterUpdateDescription(channelJid, description);
167
+ updates.push("description");
168
+ }
169
+ if (picture) {
170
+ if (picture === "remove") {
171
+ await sock.newsletterRemovePicture(channelJid);
172
+ updates.push("picture (removed)");
173
+ } else {
174
+ const media = resolveMedia(picture);
175
+ let imgBuf;
176
+ if (Buffer.isBuffer(media)) {
177
+ imgBuf = media;
178
+ } else if (media.url) {
179
+ const resp = await fetch(media.url);
180
+ imgBuf = Buffer.from(await resp.arrayBuffer());
181
+ } else {
182
+ imgBuf = media;
183
+ }
184
+ await sock.newsletterUpdatePicture(channelJid, imgBuf);
185
+ updates.push("picture");
186
+ }
187
+ }
188
+
189
+ if (updates.length === 0) {
190
+ return errResult("No updates provided. Specify name, description, or picture.");
191
+ }
192
+ return okResult({ status: "updated", jid: channelJid, updated: updates });
193
+ },
194
+ },
195
+
196
+ // 5. delete_channel
197
+ {
198
+ definition: {
199
+ name: "delete_channel",
200
+ description: "Delete a WhatsApp Channel that you own. This action is irreversible.",
201
+ inputSchema: {
202
+ type: "object",
203
+ properties: {
204
+ jid: { type: "string", description: "Channel JID to delete." },
205
+ },
206
+ required: ["jid"],
207
+ },
208
+ },
209
+ handler: async ({ jid }, { sock }) => {
210
+ const channelJid = newsletterJid(jid);
211
+ await sock.newsletterDelete(channelJid);
212
+ return okResult({ status: "deleted", jid: channelJid });
213
+ },
214
+ },
215
+ ];
@@ -0,0 +1,291 @@
1
+ /**
2
+ * whats-mcp — Chat management tools (5 tools).
3
+ *
4
+ * list_chats, get_messages, manage_chat, star_message, set_disappearing
5
+ */
6
+
7
+ "use strict";
8
+
9
+ const {
10
+ phoneToJid, isGroupJid, okResult, errResult, formatChat, formatMessage,
11
+ } = require("../helpers");
12
+ const { fetchAdditionalHistory } = require("./history-support");
13
+
14
+ module.exports = [
15
+ // 1. list_chats
16
+ {
17
+ definition: {
18
+ name: "list_chats",
19
+ description:
20
+ "List recent chats from the in-memory store." +
21
+ " Returns chat JIDs, names, timestamps, unread counts, and other metadata." +
22
+ " Results are sorted by most recent activity.",
23
+ inputSchema: {
24
+ type: "object",
25
+ properties: {
26
+ limit: { type: "integer", description: "Max number of chats to return (default 50, max 500)." },
27
+ offset: { type: "integer", description: "Offset for pagination (default 0)." },
28
+ filter: {
29
+ type: "string",
30
+ enum: ["all", "groups", "contacts", "unread"],
31
+ description: "Filter chats: all (default), groups, contacts, unread.",
32
+ },
33
+ },
34
+ },
35
+ },
36
+ handler: async ({ limit, offset, filter }, { store }) => {
37
+ let chats = store.listChats(10000);
38
+
39
+ // Apply filter
40
+ const f = filter || "all";
41
+ if (f === "groups") chats = chats.filter((c) => isGroupJid(c.id));
42
+ if (f === "contacts") chats = chats.filter((c) => !isGroupJid(c.id));
43
+ if (f === "unread") chats = chats.filter((c) => (c.unreadCount || 0) > 0);
44
+
45
+ const total = chats.length;
46
+ const off = offset || 0;
47
+ const lim = Math.min(limit || 50, 500);
48
+ const page = chats.slice(off, off + lim);
49
+
50
+ return okResult({
51
+ total,
52
+ offset: off,
53
+ count: page.length,
54
+ chats: page.map(formatChat),
55
+ });
56
+ },
57
+ },
58
+
59
+ // 2. get_messages
60
+ {
61
+ definition: {
62
+ name: "get_messages",
63
+ description:
64
+ "Get recent messages from a specific chat." +
65
+ " Messages come from the local store and can trigger an on-demand history fetch for older messages." +
66
+ " Use before_id for pagination toward older messages.",
67
+ inputSchema: {
68
+ type: "object",
69
+ properties: {
70
+ jid: { type: "string", description: "Chat JID or phone number." },
71
+ limit: { type: "integer", description: "Max number of messages to return (default 50, max 200)." },
72
+ before_id: { type: "string", description: "Message ID cursor: return messages older than this. For pagination." },
73
+ fetch_history: {
74
+ type: "boolean",
75
+ description: "If true (default), request additional older history from WhatsApp when the local cache is insufficient.",
76
+ },
77
+ history_count: {
78
+ type: "integer",
79
+ description: "How many older messages to request from WhatsApp during on-demand history sync (default: max(limit, 50), max 200).",
80
+ },
81
+ history_wait_ms: {
82
+ type: "integer",
83
+ description: "How long to wait for history sync events after requesting older messages (default 3500ms, max 15000ms).",
84
+ },
85
+ since: {
86
+ type: "integer",
87
+ description: "Unix timestamp: only include messages sent at or after this time.",
88
+ },
89
+ until: {
90
+ type: "integer",
91
+ description: "Unix timestamp: only include messages sent at or before this time.",
92
+ },
93
+ include_types: {
94
+ type: "array",
95
+ items: { type: "string" },
96
+ description: "If set, only include messages of these types (e.g. text, image, video, audio, document, voice_note, sticker, location, contact, poll).",
97
+ },
98
+ exclude_types: {
99
+ type: "array",
100
+ items: { type: "string" },
101
+ description: "Exclude messages of these types (e.g. reaction, protocol, senderKeyDistribution, unknown).",
102
+ },
103
+ },
104
+ required: ["jid"],
105
+ },
106
+ },
107
+ handler: async ({ jid, limit, before_id, fetch_history, history_count, history_wait_ms, since, until, include_types, exclude_types }, { sock, store }) => {
108
+ const chatJid = phoneToJid(jid);
109
+ const lim = Math.min(limit || 50, 200);
110
+ const filterOpts = {
111
+ since: since || undefined,
112
+ until: until || undefined,
113
+ types: include_types || undefined,
114
+ excludeTypes: exclude_types || undefined,
115
+ };
116
+ let historySync = {
117
+ enabled: fetch_history !== false,
118
+ requested: false,
119
+ received: false,
120
+ reason: "cache_sufficient",
121
+ before_count: store.countMessages(chatJid),
122
+ after_count: store.countMessages(chatJid),
123
+ };
124
+
125
+ let messages = store.getMessages(chatJid, lim, before_id, filterOpts);
126
+ const shouldFetchHistory = fetch_history !== false && (before_id || messages.length < lim);
127
+ if (shouldFetchHistory) {
128
+ historySync = await fetchAdditionalHistory({
129
+ sock,
130
+ store,
131
+ jid: chatJid,
132
+ beforeId: before_id,
133
+ limit: lim,
134
+ historyCount: history_count,
135
+ waitMs: history_wait_ms,
136
+ enabled: fetch_history !== false,
137
+ });
138
+ messages = store.getMessages(chatJid, lim, before_id, filterOpts);
139
+ }
140
+
141
+ return okResult({
142
+ jid: chatJid,
143
+ count: messages.length,
144
+ messages: messages.map(formatMessage),
145
+ history_sync: historySync,
146
+ });
147
+ },
148
+ },
149
+
150
+ // 3. manage_chat
151
+ {
152
+ definition: {
153
+ name: "manage_chat",
154
+ description:
155
+ "Perform a chat management action: archive, unarchive, pin, unpin," +
156
+ " mute, unmute, mark_read, mark_unread, delete, or clear.",
157
+ inputSchema: {
158
+ type: "object",
159
+ properties: {
160
+ jid: { type: "string", description: "Chat JID or phone number." },
161
+ action: {
162
+ type: "string",
163
+ enum: [
164
+ "archive", "unarchive",
165
+ "pin", "unpin",
166
+ "mute", "unmute",
167
+ "mark_read", "mark_unread",
168
+ "delete", "clear",
169
+ ],
170
+ description: "Action to perform.",
171
+ },
172
+ mute_duration: {
173
+ type: "integer",
174
+ description: "For 'mute' action: duration in seconds. 0 = 8 hours, -1 = forever. Default 8 hours.",
175
+ },
176
+ },
177
+ required: ["jid", "action"],
178
+ },
179
+ },
180
+ handler: async ({ jid, action, mute_duration }, { sock, store }) => {
181
+ const chatJid = phoneToJid(jid);
182
+ const now = Date.now();
183
+
184
+ // Get last messages for read/unread operations
185
+ let lastMessages;
186
+ if (action === "mark_read" || action === "mark_unread") {
187
+ const msgs = store.getMessages(chatJid, 1);
188
+ if (msgs.length > 0) {
189
+ lastMessages = [{ id: msgs[0].key.id, remoteJid: chatJid, fromMe: msgs[0].key.fromMe }];
190
+ }
191
+ }
192
+
193
+ const modMap = {
194
+ archive: { archive: true, lastMessages: undefined },
195
+ unarchive: { archive: false, lastMessages: undefined },
196
+ pin: { pin: true },
197
+ unpin: { pin: false },
198
+ mute: {
199
+ mute: mute_duration === -1
200
+ ? undefined // Will be handled below
201
+ : (mute_duration || 8 * 3600) * 1000 + now,
202
+ },
203
+ unmute: { mute: null },
204
+ mark_read: { markRead: true, lastMessages },
205
+ mark_unread: { markRead: false, lastMessages },
206
+ delete: { delete: true, lastMessages },
207
+ clear: { clear: { messages: [] } }, // clears all messages flag
208
+ };
209
+
210
+ if (action === "mute" && mute_duration === -1) {
211
+ modMap.mute.mute = 0; // 0 = mute forever in Baileys
212
+ }
213
+
214
+ const mod = modMap[action];
215
+ if (!mod) return errResult(`Unknown action: ${action}`);
216
+
217
+ // Baileys chatModify takes (modification, jid)
218
+ await sock.chatModify(mod, chatJid);
219
+
220
+ return okResult({ status: action, jid: chatJid });
221
+ },
222
+ },
223
+
224
+ // 4. star_message
225
+ {
226
+ definition: {
227
+ name: "star_message",
228
+ description: "Star or unstar a message.",
229
+ inputSchema: {
230
+ type: "object",
231
+ properties: {
232
+ jid: { type: "string", description: "Chat JID." },
233
+ message_id: { type: "string", description: "Message ID to star/unstar." },
234
+ star: { type: "boolean", description: "true to star, false to unstar. Default true." },
235
+ from_me: { type: "boolean", description: "Whether the message was sent by you. Default false." },
236
+ },
237
+ required: ["jid", "message_id"],
238
+ },
239
+ },
240
+ handler: async ({ jid, message_id, star, from_me }, { sock }) => {
241
+ const chatJid = phoneToJid(jid);
242
+ const shouldStar = star !== false;
243
+ await sock.chatModify(
244
+ {
245
+ star: {
246
+ messages: [{ id: message_id, fromMe: from_me ?? false }],
247
+ star: shouldStar,
248
+ },
249
+ },
250
+ chatJid,
251
+ );
252
+ return okResult({
253
+ status: shouldStar ? "starred" : "unstarred",
254
+ jid: chatJid,
255
+ message_id,
256
+ });
257
+ },
258
+ },
259
+
260
+ // 5. set_disappearing
261
+ {
262
+ definition: {
263
+ name: "set_disappearing",
264
+ description:
265
+ "Set disappearing messages timer for a chat." +
266
+ " Available durations: 0 (off), 86400 (24h), 604800 (7 days), 7776000 (90 days).",
267
+ inputSchema: {
268
+ type: "object",
269
+ properties: {
270
+ jid: { type: "string", description: "Chat JID." },
271
+ duration: {
272
+ type: "integer",
273
+ enum: [0, 86400, 604800, 7776000],
274
+ description: "Disappearing timer in seconds: 0=off, 86400=24h, 604800=7d, 7776000=90d.",
275
+ },
276
+ },
277
+ required: ["jid", "duration"],
278
+ },
279
+ },
280
+ handler: async ({ jid, duration }, { sock }) => {
281
+ const chatJid = phoneToJid(jid);
282
+ await sock.sendMessage(chatJid, { disappearingMessagesInChat: duration });
283
+ const labels = { 0: "off", 86400: "24 hours", 604800: "7 days", 7776000: "90 days" };
284
+ return okResult({
285
+ status: "set",
286
+ jid: chatJid,
287
+ disappearing: labels[duration] || `${duration}s`,
288
+ });
289
+ },
290
+ },
291
+ ];