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.
- package/.dockerignore +12 -0
- package/.gitlab-ci.yml +54 -0
- package/CHANGELOG.md +38 -0
- package/README.md +205 -0
- package/TODO.md +6 -0
- package/config.json +19 -0
- package/package.json +46 -0
- package/src/.env.example +21 -0
- package/src/admin/cli.js +916 -0
- package/src/admin/service.js +271 -0
- package/src/admin/telegram.js +178 -0
- package/src/admin.js +12 -0
- package/src/config.js +147 -0
- package/src/connection.js +334 -0
- package/src/helpers.js +264 -0
- package/src/http_app.js +267 -0
- package/src/index.js +4 -0
- package/src/main.js +71 -0
- package/src/server.js +67 -0
- package/src/store.js +925 -0
- package/src/tools/analytics.js +157 -0
- package/src/tools/channels.js +215 -0
- package/src/tools/chats.js +291 -0
- package/src/tools/contacts.js +259 -0
- package/src/tools/digest.js +249 -0
- package/src/tools/groups.js +529 -0
- package/src/tools/history-support.js +114 -0
- package/src/tools/labels.js +168 -0
- package/src/tools/messaging.js +510 -0
- package/src/tools/overview.js +416 -0
- package/src/tools/profile.js +155 -0
- package/src/tools/registry.js +105 -0
- package/src/tools/tags.js +104 -0
- package/src/tools/utils.js +325 -0
- package/src/tools/watchlists.js +136 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* whats-mcp — Overview & smart search tools (2 tools).
|
|
3
|
+
*
|
|
4
|
+
* whatsup — full daily overview, watchlist-prioritized, unanswered threads
|
|
5
|
+
* find_messages — smart semantic search with keyword expansion
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const { phoneToJid, isGroupJid, okResult, errResult, formatMessage } = require("../helpers");
|
|
11
|
+
|
|
12
|
+
// ── Topic expansion map (French + English) ───────────────────────────────────
|
|
13
|
+
//
|
|
14
|
+
// For each topic, lists keywords to expand the user's query into.
|
|
15
|
+
// "ia" → also searches for "llm", "gpt", "machine learning", etc.
|
|
16
|
+
// Partial matches are intentional (e.g. "opportunit" catches opportunité/opportunités).
|
|
17
|
+
|
|
18
|
+
const TOPIC_EXPANSIONS = {
|
|
19
|
+
ia: [
|
|
20
|
+
"ia", "intelligence artificielle", "ai", "machine learning", "llm", "gpt",
|
|
21
|
+
"chatgpt", "neural", "deep learning", "ml", "nlp", "modele", "modèle",
|
|
22
|
+
"mistral", "gemini", "openai", "anthropic", "claude", "transformer",
|
|
23
|
+
"rag", "embedding", "dataset", "data science",
|
|
24
|
+
],
|
|
25
|
+
stage: [
|
|
26
|
+
"stage", "internship", "alternance", "apprentissage", "stagiaire",
|
|
27
|
+
],
|
|
28
|
+
offre: [
|
|
29
|
+
"offre", "opportunit", "recrutement", "embauche", "poste", "job",
|
|
30
|
+
"emploi", "cdi", "cdd", "freelance", "mission", "contrat",
|
|
31
|
+
],
|
|
32
|
+
badminton: [
|
|
33
|
+
"badminton", "binet bad", "tournoi bad", "match bad", "entraîn",
|
|
34
|
+
"raquette", "volant", "terrain bad",
|
|
35
|
+
],
|
|
36
|
+
sport: [
|
|
37
|
+
"sport", "match", "tournoi", "training", "gym", "running", "course",
|
|
38
|
+
"séance", "terrain",
|
|
39
|
+
],
|
|
40
|
+
reunion: [
|
|
41
|
+
"réunion", "reunion", "meeting", "présentiel", "visio", "conf",
|
|
42
|
+
"appel", "call", "zoom", "rdv", "rendez-vous", "rencontre",
|
|
43
|
+
],
|
|
44
|
+
urgence: [
|
|
45
|
+
"urgent", "urgence", "asap", "rapidement", "help", "aide",
|
|
46
|
+
"besoin", "au plus vite", "dès que",
|
|
47
|
+
],
|
|
48
|
+
evenement: [
|
|
49
|
+
"event", "événement", "soirée", "sortie", "fête", "party",
|
|
50
|
+
"voyage", "trip", "hackathon", "datathon", "conférence",
|
|
51
|
+
"séminaire", "workshop",
|
|
52
|
+
],
|
|
53
|
+
action: [
|
|
54
|
+
"action à", "peux-tu", "pourras-tu", "peux tu", "merci de",
|
|
55
|
+
"il faut", "n'oublie pas", "to do", "todo", "rappel",
|
|
56
|
+
"reminder", "deadline", "échéance", "date limite", "à faire",
|
|
57
|
+
"pense à",
|
|
58
|
+
],
|
|
59
|
+
logement: [
|
|
60
|
+
"logement", "appart", "appartement", "coloc", "colocation",
|
|
61
|
+
"loyer", "chambre", "résidence", "hébergement", "housing", "rent",
|
|
62
|
+
],
|
|
63
|
+
bourse: [
|
|
64
|
+
"bourse", "scholarship", "financement", "aide financière",
|
|
65
|
+
"subvention", "grant", "fellowship",
|
|
66
|
+
],
|
|
67
|
+
annonce: [
|
|
68
|
+
"annonce", "annoncé", "communiqué", "info", "rappel",
|
|
69
|
+
"important", "attention", "note",
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Expand a user query to related keywords using the topic map.
|
|
75
|
+
* Returns [original_query, ...expanded_keywords] (deduped, lowercase).
|
|
76
|
+
*/
|
|
77
|
+
function _expandQuery(query) {
|
|
78
|
+
const lower = query.toLowerCase().trim();
|
|
79
|
+
const keywords = new Set([lower]);
|
|
80
|
+
|
|
81
|
+
for (const [topic, expansions] of Object.entries(TOPIC_EXPANSIONS)) {
|
|
82
|
+
const matched =
|
|
83
|
+
lower.includes(topic) ||
|
|
84
|
+
expansions.some((e) => lower.includes(e));
|
|
85
|
+
if (matched) {
|
|
86
|
+
for (const e of expansions) keywords.add(e);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return Array.from(keywords);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Collect all JIDs from all watchlists (store + config fallback).
|
|
95
|
+
*/
|
|
96
|
+
function _allWatchlistJids(store, config) {
|
|
97
|
+
const storeWLs = store.listWatchlists();
|
|
98
|
+
const configWLs = config?.watchlists || {};
|
|
99
|
+
const merged = { ...configWLs, ...storeWLs };
|
|
100
|
+
const jidSet = new Set();
|
|
101
|
+
for (const jids of Object.values(merged)) {
|
|
102
|
+
for (const jid of jids) jidSet.add(phoneToJid(jid));
|
|
103
|
+
}
|
|
104
|
+
return { jidSet, merged };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = [
|
|
108
|
+
// 1. whatsup — Daily overview
|
|
109
|
+
{
|
|
110
|
+
definition: {
|
|
111
|
+
name: "whatsup",
|
|
112
|
+
description:
|
|
113
|
+
"DAILY WHATSAPP OVERVIEW — CALL THIS AUTOMATICALLY when user asks:" +
|
|
114
|
+
" 'what's up', 'quoi de neuf', 'résume ma journée WhatsApp', 'qu'est-ce que j'ai manqué'," +
|
|
115
|
+
" 'donne-moi un résumé', 'c\\'est quoi les news', 'update WhatsApp', 'mon WhatsApp'," +
|
|
116
|
+
" 'koi de neuf', 'les actus WA', 'donne moi l\\'overview', 'briefing whatsapp'," +
|
|
117
|
+
" or any similar request about today's WhatsApp activity." +
|
|
118
|
+
" Returns a complete structured overview from midnight today to now:" +
|
|
119
|
+
" (1) Watchlist chats first (your prioritized groups) with all today's messages;" +
|
|
120
|
+
" (2) Other active chats with today's messages;" +
|
|
121
|
+
" (3) Needs-reply — chats where the last message is incoming, waiting for your response." +
|
|
122
|
+
" No parameters required for default use — call with empty args {}.",
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: "object",
|
|
125
|
+
properties: {
|
|
126
|
+
since: {
|
|
127
|
+
type: "integer",
|
|
128
|
+
description: "Start Unix timestamp. Default: midnight today.",
|
|
129
|
+
},
|
|
130
|
+
until: {
|
|
131
|
+
type: "integer",
|
|
132
|
+
description: "End Unix timestamp. Default: now.",
|
|
133
|
+
},
|
|
134
|
+
watchlists: {
|
|
135
|
+
type: "array",
|
|
136
|
+
items: { type: "string" },
|
|
137
|
+
description: "Only show these watchlists (default: all).",
|
|
138
|
+
},
|
|
139
|
+
limit_per_chat: {
|
|
140
|
+
type: "integer",
|
|
141
|
+
description: "Max messages per chat (default: 50, max: 200).",
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
handler: async ({ since, until, watchlists: wlFilter, limit_per_chat }, { store, config }) => {
|
|
147
|
+
const now = Math.floor(Date.now() / 1000);
|
|
148
|
+
|
|
149
|
+
// Default period: since midnight today
|
|
150
|
+
const todayStart = new Date();
|
|
151
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
152
|
+
const effectiveSince = since || Math.floor(todayStart.getTime() / 1000);
|
|
153
|
+
const effectiveUntil = until || now;
|
|
154
|
+
const lim = Math.min(limit_per_chat || 50, 200);
|
|
155
|
+
|
|
156
|
+
// Build JID → watchlist names mapping
|
|
157
|
+
const storeWLs = store.listWatchlists();
|
|
158
|
+
const configWLs = config?.watchlists || {};
|
|
159
|
+
const allWLs = { ...configWLs, ...storeWLs };
|
|
160
|
+
|
|
161
|
+
const activeWLs =
|
|
162
|
+
wlFilter && wlFilter.length > 0
|
|
163
|
+
? Object.fromEntries(Object.entries(allWLs).filter(([n]) => wlFilter.includes(n)))
|
|
164
|
+
: allWLs;
|
|
165
|
+
|
|
166
|
+
const jidToWatchlists = new Map();
|
|
167
|
+
for (const [wlName, jids] of Object.entries(activeWLs)) {
|
|
168
|
+
for (const rawJid of jids) {
|
|
169
|
+
const jid = phoneToJid(rawJid);
|
|
170
|
+
if (!jidToWatchlists.has(jid)) jidToWatchlists.set(jid, []);
|
|
171
|
+
jidToWatchlists.get(jid).push(wlName);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const watchlistJidSet = new Set(jidToWatchlists.keys());
|
|
175
|
+
|
|
176
|
+
const filterOpts = {
|
|
177
|
+
since: effectiveSince,
|
|
178
|
+
until: effectiveUntil,
|
|
179
|
+
excludeTypes: ["protocol", "reaction"],
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const watchlistChats = [];
|
|
183
|
+
const otherChats = [];
|
|
184
|
+
const needsReplyChats = [];
|
|
185
|
+
|
|
186
|
+
// All JIDs to scan (messages + watchlist JIDs so watched chats always appear)
|
|
187
|
+
const allJids = new Set(store.messages.keys());
|
|
188
|
+
for (const jid of watchlistJidSet) allJids.add(jid);
|
|
189
|
+
|
|
190
|
+
for (const jid of allJids) {
|
|
191
|
+
if (jid === "status@broadcast") continue;
|
|
192
|
+
|
|
193
|
+
const messages = store.getMessages(jid, lim, undefined, filterOpts);
|
|
194
|
+
const formatted = messages.map(formatMessage).filter(Boolean);
|
|
195
|
+
if (formatted.length === 0) continue;
|
|
196
|
+
|
|
197
|
+
const chat = store.getChat(jid);
|
|
198
|
+
const contact = store.getContact(jid);
|
|
199
|
+
const chatName =
|
|
200
|
+
chat?.name || chat?.subject || contact?.name || contact?.notify || jid;
|
|
201
|
+
|
|
202
|
+
// "needs reply": last non-protocol message is not from me
|
|
203
|
+
const recent = store.getMessages(jid, 3, undefined, { excludeTypes: ["protocol", "reaction"] });
|
|
204
|
+
const recentFormatted = recent.map(formatMessage).filter(Boolean);
|
|
205
|
+
const lastMsg = recentFormatted[0]; // newest first
|
|
206
|
+
const needsReply = !!lastMsg && !lastMsg.from_me;
|
|
207
|
+
|
|
208
|
+
const chatData = {
|
|
209
|
+
jid,
|
|
210
|
+
name: chatName,
|
|
211
|
+
is_group: isGroupJid(jid),
|
|
212
|
+
unread: chat?.unreadCount || 0,
|
|
213
|
+
message_count: formatted.length,
|
|
214
|
+
needs_reply: needsReply,
|
|
215
|
+
last_message_time: formatted[0]?.timestamp || null,
|
|
216
|
+
messages: formatted,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
if (watchlistJidSet.has(jid)) {
|
|
220
|
+
chatData.watchlists = jidToWatchlists.get(jid);
|
|
221
|
+
watchlistChats.push(chatData);
|
|
222
|
+
} else {
|
|
223
|
+
otherChats.push(chatData);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (needsReply) {
|
|
227
|
+
needsReplyChats.push({
|
|
228
|
+
jid,
|
|
229
|
+
name: chatName,
|
|
230
|
+
is_group: isGroupJid(jid),
|
|
231
|
+
in_watchlist: watchlistJidSet.has(jid),
|
|
232
|
+
last_message: lastMsg,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Sort by latest activity
|
|
238
|
+
const byTime = (a, b) => (b.last_message_time || 0) - (a.last_message_time || 0);
|
|
239
|
+
watchlistChats.sort(byTime);
|
|
240
|
+
otherChats.sort(byTime);
|
|
241
|
+
// needs_reply: watchlist first, then by last activity
|
|
242
|
+
needsReplyChats.sort((a, b) => {
|
|
243
|
+
if (a.in_watchlist !== b.in_watchlist) return b.in_watchlist ? 1 : -1;
|
|
244
|
+
return (b.last_message?.timestamp || 0) - (a.last_message?.timestamp || 0);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const totalMessages =
|
|
248
|
+
watchlistChats.reduce((s, c) => s + c.message_count, 0) +
|
|
249
|
+
otherChats.reduce((s, c) => s + c.message_count, 0);
|
|
250
|
+
|
|
251
|
+
return okResult({
|
|
252
|
+
date: new Date().toLocaleDateString("fr-FR"),
|
|
253
|
+
period: {
|
|
254
|
+
since: effectiveSince,
|
|
255
|
+
until: effectiveUntil,
|
|
256
|
+
from: new Date(effectiveSince * 1000).toLocaleTimeString("fr-FR", {
|
|
257
|
+
hour: "2-digit",
|
|
258
|
+
minute: "2-digit",
|
|
259
|
+
}),
|
|
260
|
+
to: new Date(effectiveUntil * 1000).toLocaleTimeString("fr-FR", {
|
|
261
|
+
hour: "2-digit",
|
|
262
|
+
minute: "2-digit",
|
|
263
|
+
}),
|
|
264
|
+
},
|
|
265
|
+
summary: {
|
|
266
|
+
total_active_chats: watchlistChats.length + otherChats.length,
|
|
267
|
+
watchlist_chats: watchlistChats.length,
|
|
268
|
+
other_chats: otherChats.length,
|
|
269
|
+
total_messages: totalMessages,
|
|
270
|
+
needs_reply_count: needsReplyChats.length,
|
|
271
|
+
},
|
|
272
|
+
watchlist_chats: watchlistChats,
|
|
273
|
+
other_chats: otherChats,
|
|
274
|
+
needs_reply: needsReplyChats,
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
// 2. find_messages — Smart semantic search
|
|
280
|
+
{
|
|
281
|
+
definition: {
|
|
282
|
+
name: "find_messages",
|
|
283
|
+
description:
|
|
284
|
+
"SMART SEMANTIC MESSAGE SEARCH — CALL THIS when user asks about specific topics:" +
|
|
285
|
+
" 'y a-t-il des messages sur l\\'IA', 'des offres de stage', 'des attentes pour moi'," +
|
|
286
|
+
" 'des actions à faire', 'des events à venir', 'des invitations', 'des urgences'," +
|
|
287
|
+
" 'what about jobs', 'any urgent messages', 'des réunions prévues', 'infos sur X'," +
|
|
288
|
+
" 'des messages importants', 'quoi de neuf sur l\\'IA', 'y a-t-il des stages'." +
|
|
289
|
+
" Performs intelligent multi-keyword search with automatic topic expansion:" +
|
|
290
|
+
" 'ia' also searches for 'machine learning', 'LLM', 'GPT', 'intelligence artificielle', etc." +
|
|
291
|
+
" 'offre' also expands to 'stage', 'emploi', 'recrutement', 'opportunité', etc." +
|
|
292
|
+
" Results are ALWAYS prioritized: watchlist chats first, then all other chats." +
|
|
293
|
+
" Groups results by chat. Covers entire message history by default.",
|
|
294
|
+
inputSchema: {
|
|
295
|
+
type: "object",
|
|
296
|
+
properties: {
|
|
297
|
+
query: {
|
|
298
|
+
type: "string",
|
|
299
|
+
description:
|
|
300
|
+
"Topic or question to search for (French or English)." +
|
|
301
|
+
" Examples: 'IA', 'stage', 'offres emploi', 'urgence', 'events', 'actions à faire'.",
|
|
302
|
+
},
|
|
303
|
+
since: {
|
|
304
|
+
type: "integer",
|
|
305
|
+
description: "Optional: only include messages after this Unix timestamp.",
|
|
306
|
+
},
|
|
307
|
+
until: {
|
|
308
|
+
type: "integer",
|
|
309
|
+
description: "Optional: only include messages before this Unix timestamp.",
|
|
310
|
+
},
|
|
311
|
+
limit: {
|
|
312
|
+
type: "integer",
|
|
313
|
+
description: "Max total results (default: 80, max: 300).",
|
|
314
|
+
},
|
|
315
|
+
watchlist_only: {
|
|
316
|
+
type: "boolean",
|
|
317
|
+
description: "If true, restrict search to watchlist chats only.",
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
required: ["query"],
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
handler: async ({ query, since, until, limit, watchlist_only }, { store, config }) => {
|
|
324
|
+
if (!query || !query.trim()) {
|
|
325
|
+
return errResult("Parameter 'query' is required.");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const capped = Math.min(limit || 80, 300);
|
|
329
|
+
const opts = {
|
|
330
|
+
since: since || undefined,
|
|
331
|
+
until: until || undefined,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// Build watchlist set
|
|
335
|
+
const { jidSet: watchlistJidSet } = _allWatchlistJids(store, config);
|
|
336
|
+
|
|
337
|
+
// Expand query into related keywords
|
|
338
|
+
const keywords = _expandQuery(query);
|
|
339
|
+
|
|
340
|
+
// Phase 1: TF-IDF analytics search with expanded query (handles tokenization internally)
|
|
341
|
+
const expandedQuery = keywords.join(" ");
|
|
342
|
+
const analyticsResults = store.analyticsSearch(expandedQuery, null, capped, opts);
|
|
343
|
+
|
|
344
|
+
// Phase 2: plain text fallback for original query (phrase match catches more)
|
|
345
|
+
const textResults = store.searchMessages(query, null, Math.floor(capped / 2), opts);
|
|
346
|
+
|
|
347
|
+
// Merge: analytics first (higher quality), then add text-only misses
|
|
348
|
+
const seenIds = new Set(analyticsResults.map((r) => r.id));
|
|
349
|
+
const allResults = [...analyticsResults];
|
|
350
|
+
for (const r of textResults) {
|
|
351
|
+
if (!seenIds.has(r.id)) {
|
|
352
|
+
allResults.push({ ...r, score: 0.3, matched_terms: [query] });
|
|
353
|
+
seenIds.add(r.id);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Filter by watchlist if requested
|
|
358
|
+
let filtered = watchlist_only
|
|
359
|
+
? allResults.filter((r) => watchlistJidSet.has(r.from))
|
|
360
|
+
: allResults;
|
|
361
|
+
|
|
362
|
+
// Sort: watchlist first → score desc → recency desc
|
|
363
|
+
filtered.sort((a, b) => {
|
|
364
|
+
const aWL = watchlistJidSet.has(a.from) ? 1 : 0;
|
|
365
|
+
const bWL = watchlistJidSet.has(b.from) ? 1 : 0;
|
|
366
|
+
if (aWL !== bWL) return bWL - aWL;
|
|
367
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
368
|
+
return (b.timestamp || 0) - (a.timestamp || 0);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
filtered = filtered.slice(0, capped);
|
|
372
|
+
|
|
373
|
+
// Group by chat JID
|
|
374
|
+
const byChat = new Map();
|
|
375
|
+
for (const r of filtered) {
|
|
376
|
+
const chatJid = r.from; // formatMessage sets from = key.remoteJid = chat JID
|
|
377
|
+
if (!byChat.has(chatJid)) {
|
|
378
|
+
const chat = store.getChat(chatJid);
|
|
379
|
+
const contact = store.getContact(chatJid);
|
|
380
|
+
byChat.set(chatJid, {
|
|
381
|
+
jid: chatJid,
|
|
382
|
+
name: chat?.name || chat?.subject || contact?.name || contact?.notify || chatJid,
|
|
383
|
+
is_group: isGroupJid(chatJid),
|
|
384
|
+
in_watchlist: watchlistJidSet.has(chatJid),
|
|
385
|
+
messages: [],
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
byChat.get(chatJid).messages.push({
|
|
389
|
+
id: r.id,
|
|
390
|
+
timestamp: r.timestamp,
|
|
391
|
+
from_me: r.from_me,
|
|
392
|
+
participant: r.participant,
|
|
393
|
+
push_name: r.push_name,
|
|
394
|
+
type: r.type,
|
|
395
|
+
text: r.text,
|
|
396
|
+
matched_keywords: r.matched_terms || [query],
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Sort chats: watchlist first → most messages first
|
|
401
|
+
const chatResults = Array.from(byChat.values()).sort((a, b) => {
|
|
402
|
+
if (a.in_watchlist !== b.in_watchlist) return b.in_watchlist ? 1 : -1;
|
|
403
|
+
return b.messages.length - a.messages.length;
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
return okResult({
|
|
407
|
+
query,
|
|
408
|
+
expanded_keywords: keywords.length > 1 ? keywords.slice(1) : [],
|
|
409
|
+
total_messages: filtered.length,
|
|
410
|
+
total_chats: chatResults.length,
|
|
411
|
+
watchlist_matches: chatResults.filter((c) => c.in_watchlist).length,
|
|
412
|
+
chats: chatResults,
|
|
413
|
+
});
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
];
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* whats-mcp — Profile & Privacy tools (4 tools).
|
|
3
|
+
*
|
|
4
|
+
* update_display_name, update_about, update_profile_picture, manage_privacy
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const { resolveMedia, okResult, errResult } = require("../helpers");
|
|
10
|
+
|
|
11
|
+
module.exports = [
|
|
12
|
+
// 1. update_display_name
|
|
13
|
+
{
|
|
14
|
+
definition: {
|
|
15
|
+
name: "update_display_name",
|
|
16
|
+
description: "Change your WhatsApp display name.",
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
name: { type: "string", description: "New display name (max 25 characters)." },
|
|
21
|
+
},
|
|
22
|
+
required: ["name"],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
handler: async ({ name }, { sock }) => {
|
|
26
|
+
if (!name || name.length > 25) {
|
|
27
|
+
return errResult("Name must be between 1 and 25 characters.");
|
|
28
|
+
}
|
|
29
|
+
await sock.updateProfileName(name);
|
|
30
|
+
return okResult({ status: "updated", name });
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
// 2. update_about
|
|
35
|
+
{
|
|
36
|
+
definition: {
|
|
37
|
+
name: "update_about",
|
|
38
|
+
description: "Change your WhatsApp 'About' status text.",
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: {
|
|
42
|
+
text: { type: "string", description: "New about text (max 139 characters). Empty string to clear." },
|
|
43
|
+
},
|
|
44
|
+
required: ["text"],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
handler: async ({ text }, { sock }) => {
|
|
48
|
+
await sock.updateProfileStatus(text || "");
|
|
49
|
+
return okResult({ status: "updated", about: text });
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// 3. update_profile_picture
|
|
54
|
+
{
|
|
55
|
+
definition: {
|
|
56
|
+
name: "update_profile_picture",
|
|
57
|
+
description: "Change your WhatsApp profile picture.",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
source: { type: "string", description: "Image source: URL, base64, or local file path. Use 'remove' to delete the picture." },
|
|
62
|
+
},
|
|
63
|
+
required: ["source"],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
handler: async ({ source }, { sock }) => {
|
|
67
|
+
if (source === "remove") {
|
|
68
|
+
await sock.removeProfilePicture(sock.user.id);
|
|
69
|
+
return okResult({ status: "removed" });
|
|
70
|
+
}
|
|
71
|
+
const media = resolveMedia(source);
|
|
72
|
+
let imgBuf;
|
|
73
|
+
if (Buffer.isBuffer(media)) {
|
|
74
|
+
imgBuf = media;
|
|
75
|
+
} else if (media.url) {
|
|
76
|
+
const resp = await fetch(media.url);
|
|
77
|
+
imgBuf = Buffer.from(await resp.arrayBuffer());
|
|
78
|
+
} else {
|
|
79
|
+
imgBuf = media;
|
|
80
|
+
}
|
|
81
|
+
await sock.updateProfilePicture(sock.user.id, imgBuf);
|
|
82
|
+
return okResult({ status: "updated" });
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// 4. manage_privacy
|
|
87
|
+
{
|
|
88
|
+
definition: {
|
|
89
|
+
name: "manage_privacy",
|
|
90
|
+
description:
|
|
91
|
+
"Get or update WhatsApp privacy settings." +
|
|
92
|
+
" Use action='get' to retrieve current settings." +
|
|
93
|
+
" Use action='set' with a setting name and value to update.",
|
|
94
|
+
inputSchema: {
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {
|
|
97
|
+
action: {
|
|
98
|
+
type: "string",
|
|
99
|
+
enum: ["get", "set"],
|
|
100
|
+
description: "Action: 'get' to retrieve all privacy settings, 'set' to update one.",
|
|
101
|
+
},
|
|
102
|
+
setting: {
|
|
103
|
+
type: "string",
|
|
104
|
+
enum: [
|
|
105
|
+
"last_seen", "online", "profile_picture", "about",
|
|
106
|
+
"read_receipts", "groups_add", "default_disappearing",
|
|
107
|
+
],
|
|
108
|
+
description: "Privacy setting to update (required for 'set').",
|
|
109
|
+
},
|
|
110
|
+
value: {
|
|
111
|
+
type: "string",
|
|
112
|
+
enum: ["all", "contacts", "contact_blacklist", "none", "match_last_seen"],
|
|
113
|
+
description:
|
|
114
|
+
"New value for the setting (required for 'set')." +
|
|
115
|
+
" 'all' = everyone, 'contacts' = contacts only, 'none' = nobody." +
|
|
116
|
+
" 'contact_blacklist' = contacts except... 'match_last_seen' = match last seen setting.",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
required: ["action"],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
handler: async ({ action, setting, value }, { sock }) => {
|
|
123
|
+
if (action === "get") {
|
|
124
|
+
const settings = await sock.fetchPrivacySettings(true);
|
|
125
|
+
return okResult({ privacy: settings });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (action === "set") {
|
|
129
|
+
if (!setting || !value) {
|
|
130
|
+
return errResult("Both 'setting' and 'value' are required for 'set' action.");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const apiMap = {
|
|
134
|
+
last_seen: () => sock.updateLastSeenPrivacy(value),
|
|
135
|
+
online: () => sock.updateOnlinePrivacy(value),
|
|
136
|
+
profile_picture: () => sock.updateProfilePicturePrivacy(value),
|
|
137
|
+
about: () => sock.updateStatusPrivacy(value),
|
|
138
|
+
read_receipts: () => sock.updateReadReceiptsPrivacy(value),
|
|
139
|
+
groups_add: () => sock.updateGroupsAddPrivacy(value),
|
|
140
|
+
default_disappearing: () => sock.updateDefaultDisappearingMode(
|
|
141
|
+
value === "all" ? 0 : value === "contacts" ? 86400 : 604800,
|
|
142
|
+
),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const fn = apiMap[setting];
|
|
146
|
+
if (!fn) return errResult(`Unknown setting: ${setting}.`);
|
|
147
|
+
|
|
148
|
+
await fn();
|
|
149
|
+
return okResult({ status: "updated", setting, value });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return errResult(`Unknown action: ${action}`);
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
];
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* whats-mcp — Tool Registry.
|
|
3
|
+
*
|
|
4
|
+
* Collects all tool definitions from category modules and provides:
|
|
5
|
+
* - listTools() → array of MCP tool definitions
|
|
6
|
+
* - callTool(name, args, context) → MCP CallTool result
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
"use strict";
|
|
10
|
+
|
|
11
|
+
const messagingTools = require("./messaging");
|
|
12
|
+
const chatsTools = require("./chats");
|
|
13
|
+
const contactsTools = require("./contacts");
|
|
14
|
+
const groupsTools = require("./groups");
|
|
15
|
+
const profileTools = require("./profile");
|
|
16
|
+
const channelsTools = require("./channels");
|
|
17
|
+
const labelsTools = require("./labels");
|
|
18
|
+
const analyticsTools = require("./analytics");
|
|
19
|
+
const utilsTools = require("./utils");
|
|
20
|
+
const digestTools = require("./digest");
|
|
21
|
+
const tagsTools = require("./tags");
|
|
22
|
+
const watchlistsTools = require("./watchlists");
|
|
23
|
+
const overviewTools = require("./overview");
|
|
24
|
+
|
|
25
|
+
// ── Collect all tools ────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const ALL_TOOLS = [
|
|
28
|
+
...messagingTools,
|
|
29
|
+
...chatsTools,
|
|
30
|
+
...contactsTools,
|
|
31
|
+
...groupsTools,
|
|
32
|
+
...profileTools,
|
|
33
|
+
...channelsTools,
|
|
34
|
+
...labelsTools,
|
|
35
|
+
...analyticsTools,
|
|
36
|
+
...utilsTools,
|
|
37
|
+
...digestTools,
|
|
38
|
+
...tagsTools,
|
|
39
|
+
...watchlistsTools,
|
|
40
|
+
...overviewTools,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Build lookup maps
|
|
44
|
+
const _definitions = ALL_TOOLS.map((t) => t.definition);
|
|
45
|
+
const _handlers = new Map();
|
|
46
|
+
for (const t of ALL_TOOLS) {
|
|
47
|
+
if (_handlers.has(t.definition.name)) {
|
|
48
|
+
throw new Error(`Duplicate tool name: ${t.definition.name}`);
|
|
49
|
+
}
|
|
50
|
+
_handlers.set(t.definition.name, t.handler);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Return all MCP tool definitions for ListTools.
|
|
57
|
+
*/
|
|
58
|
+
function listTools() {
|
|
59
|
+
return _definitions;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Dispatch a CallTool request.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} name - Tool name
|
|
66
|
+
* @param {object} args - Tool arguments (from MCP request)
|
|
67
|
+
* @param {object} ctx - Context: { sock, store, connectionInfo, toolDefs }
|
|
68
|
+
* @returns {Promise<object>} MCP CallTool result ({ content: [...] })
|
|
69
|
+
*/
|
|
70
|
+
async function callTool(name, args, ctx) {
|
|
71
|
+
const handler = _handlers.get(name);
|
|
72
|
+
if (!handler) {
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
|
|
75
|
+
isError: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Provide toolDefs in context for the guide tool
|
|
81
|
+
ctx.toolDefs = _definitions;
|
|
82
|
+
return await handler(args || {}, ctx);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
// Structured error response
|
|
85
|
+
const errorMessage = err.data
|
|
86
|
+
? `${err.message} — ${JSON.stringify(err.data)}`
|
|
87
|
+
: err.message || String(err);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
content: [
|
|
91
|
+
{
|
|
92
|
+
type: "text",
|
|
93
|
+
text: JSON.stringify({
|
|
94
|
+
error: errorMessage,
|
|
95
|
+
tool: name,
|
|
96
|
+
...(err.statusCode ? { status_code: err.statusCode } : {}),
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
isError: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { listTools, callTool, ALL_TOOLS };
|