whats-mcp 0.1.0 ā 0.2.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/.gitlab-ci.yml +30 -5
- package/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/Makefile +44 -0
- package/README.md +9 -1
- package/package.json +12 -10
- package/src/tools/utils.js +72 -9
package/.gitlab-ci.yml
CHANGED
|
@@ -2,13 +2,38 @@ stages:
|
|
|
2
2
|
- validate
|
|
3
3
|
- deploy
|
|
4
4
|
|
|
5
|
+
variables:
|
|
6
|
+
DOCKER_DRIVER: overlay2
|
|
7
|
+
|
|
5
8
|
validate_project:
|
|
6
9
|
stage: validate
|
|
7
10
|
image: node:22-bookworm-slim
|
|
11
|
+
variables:
|
|
12
|
+
GIT_CLEAN_FLAGS: none
|
|
8
13
|
script:
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
14
|
+
- |
|
|
15
|
+
if command -v npm >/dev/null 2>&1; then
|
|
16
|
+
apt-get update -qq && apt-get install -y --no-install-recommends git
|
|
17
|
+
git config --global url."https://github.com/".insteadOf "git@github.com:"
|
|
18
|
+
npm install
|
|
19
|
+
npm test -- --runInBand
|
|
20
|
+
node -e "const { loadConfig } = require('./src/config'); const cfg = loadConfig(); if (cfg.server.name !== 'whats-mcp') throw new Error('Unexpected package metadata wiring'); console.log('whats-mcp config smoke test OK');"
|
|
21
|
+
elif command -v docker >/dev/null 2>&1; then
|
|
22
|
+
docker run --rm \
|
|
23
|
+
-v "$CI_PROJECT_DIR:/work" \
|
|
24
|
+
-w /work \
|
|
25
|
+
node:22-bookworm-slim \
|
|
26
|
+
sh -c '
|
|
27
|
+
apt-get update -qq && apt-get install -y --no-install-recommends git &&
|
|
28
|
+
git config --global url."https://github.com/".insteadOf "git@github.com:" &&
|
|
29
|
+
npm install &&
|
|
30
|
+
npm test -- --runInBand &&
|
|
31
|
+
node -e "const { loadConfig } = require(\"./src/config\"); const cfg = loadConfig(); if (cfg.server.name !== \"whats-mcp\") throw new Error(\"Unexpected package metadata wiring\"); console.log(\"whats-mcp config smoke test OK\");"
|
|
32
|
+
'
|
|
33
|
+
else
|
|
34
|
+
echo "Neither npm nor docker is available on this runner."
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
12
37
|
|
|
13
38
|
deploy_homelab:
|
|
14
39
|
stage: deploy
|
|
@@ -21,7 +46,7 @@ deploy_homelab:
|
|
|
21
46
|
needs:
|
|
22
47
|
- validate_project
|
|
23
48
|
script:
|
|
24
|
-
- echo "
|
|
49
|
+
- echo "Deploying whats-mcp to the homelab..."
|
|
25
50
|
- cd deploy
|
|
26
51
|
- echo "WHATSAPP_STATE_DIR=/data/state" > .env
|
|
27
52
|
- echo "WHATSAPP_LOG_LEVEL=error" >> .env
|
|
@@ -50,5 +75,5 @@ deploy_homelab:
|
|
|
50
75
|
sleep 3
|
|
51
76
|
done
|
|
52
77
|
- docker inspect --format '{{.State.Health.Status}}' whats-mcp | grep -qx healthy
|
|
53
|
-
- docker exec whats-mcp
|
|
78
|
+
- docker exec whats-mcp bun -e "fetch('http://127.0.0.1:8092/health').then((r) => { if (!r.ok) process.exit(1); }).catch(() => process.exit(1))"
|
|
54
79
|
- docker image prune -f
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@ All notable changes to **whats-mcp** will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.2.0]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Local Media Cache Tooling** ā `download_media` now saves extracted Baileys buffers to local disk (`$HOME/.cache/whats_media/`) and returns the paths instead of flooding the MCP context with multi-megabyte Base64 strings.
|
|
11
|
+
- **`cleanup_media` tool** ā added a new utility tool to clear the local media cache directory and free up space safely.
|
|
12
|
+
|
|
7
13
|
### Changed
|
|
8
14
|
|
|
9
15
|
- **Project rename finalized** ā the public package and operator surface now consistently use `whats-mcp` / `whats-admin`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KAMDEM Ivann (KĻX)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/Makefile
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
.DEFAULT_GOAL := help
|
|
2
|
+
ZSH_LOGIN := zsh -lc
|
|
3
|
+
|
|
4
|
+
help: ## Show available targets
|
|
5
|
+
@grep -E '^[a-zA-Z_-]+:.*?##' $(MAKEFILE_LIST) | \
|
|
6
|
+
awk 'BEGIN {FS = ":.*?## "}; {printf " %-15s %s\n", $$1, $$2}'
|
|
7
|
+
|
|
8
|
+
# āā Dev āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
9
|
+
|
|
10
|
+
install: ## Install dependencies
|
|
11
|
+
@bun install
|
|
12
|
+
|
|
13
|
+
test: ## Run tests
|
|
14
|
+
@bun test
|
|
15
|
+
|
|
16
|
+
# āā Build & Publish āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
17
|
+
|
|
18
|
+
build: ## Bundle with bun
|
|
19
|
+
@bun run build
|
|
20
|
+
|
|
21
|
+
publish: build ## Publish to npm (requires NPM_TOKEN via bw-env)
|
|
22
|
+
@$(ZSH_LOGIN) 'if ! env | grep -q "^NPM_TOKEN="; then \
|
|
23
|
+
echo "NPM_TOKEN missing ā run bw-env first"; exit 1; fi; \
|
|
24
|
+
bun publish'
|
|
25
|
+
|
|
26
|
+
release: test build publish push ## Full release: test ā build ā publish ā push
|
|
27
|
+
|
|
28
|
+
# āā Git āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
29
|
+
|
|
30
|
+
push: ## Push current branch to all remotes (github + gitlab)
|
|
31
|
+
@branch="$$(git branch --show-current)"; \
|
|
32
|
+
for remote in $$(git remote); do \
|
|
33
|
+
echo "==> pushing $$branch to $$remote"; \
|
|
34
|
+
git push "$$remote" "$$branch"; \
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
push-tags: ## Push all tags to all remotes
|
|
38
|
+
@for remote in $$(git remote); do git push "$$remote" --tags; done
|
|
39
|
+
|
|
40
|
+
status: ## git status --short
|
|
41
|
+
@git status --short
|
|
42
|
+
|
|
43
|
+
log: ## Last 10 commits oneline
|
|
44
|
+
@git log --oneline -10
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# whats-mcp
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/whats-mcp)
|
|
4
|
+
[](https://www.npmjs.com/package/whats-mcp)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
3
7
|
Comprehensive **WhatsApp MCP server** powered by Baileys, with intent-first read surfaces and dual transport support:
|
|
4
8
|
|
|
5
9
|
- local `stdio` for direct MCP clients
|
|
@@ -47,7 +51,11 @@ src/
|
|
|
47
51
|
## Installation
|
|
48
52
|
|
|
49
53
|
```bash
|
|
50
|
-
|
|
54
|
+
# recommended ā installs globally as a standalone tool
|
|
55
|
+
npm install -g whats-mcp
|
|
56
|
+
|
|
57
|
+
# or via npx (no install)
|
|
58
|
+
npx whats-mcp
|
|
51
59
|
```
|
|
52
60
|
|
|
53
61
|
Commands exposed:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whats-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "MCP server for WhatsApp ā intent-first messaging, chat management, analytics, and operator surfaces over stdio and HTTP.",
|
|
5
5
|
"main": "src/main.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,15 +8,16 @@
|
|
|
8
8
|
"whats-admin": "src/admin.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"start": "
|
|
12
|
-
"serve:http": "
|
|
13
|
-
"admin": "
|
|
14
|
-
"login": "
|
|
15
|
-
"login:code": "
|
|
16
|
-
"
|
|
17
|
-
"test
|
|
18
|
-
"
|
|
19
|
-
"
|
|
11
|
+
"start": "bun src/main.js serve",
|
|
12
|
+
"serve:http": "bun src/main.js serve-http",
|
|
13
|
+
"admin": "bun src/admin.js",
|
|
14
|
+
"login": "bun src/admin.js login",
|
|
15
|
+
"login:code": "bun src/admin.js login --code",
|
|
16
|
+
"build": "echo 'No build required'",
|
|
17
|
+
"test": "jest --coverage",
|
|
18
|
+
"test:watch": "jest --watch",
|
|
19
|
+
"stop": "bun src/admin.js server stop",
|
|
20
|
+
"status": "bun src/admin.js status"
|
|
20
21
|
},
|
|
21
22
|
"keywords": [
|
|
22
23
|
"whatsapp",
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
"license": "MIT",
|
|
31
32
|
"type": "commonjs",
|
|
32
33
|
"engines": {
|
|
34
|
+
"bun": ">=1.0.0",
|
|
33
35
|
"node": ">=18.0.0"
|
|
34
36
|
},
|
|
35
37
|
"dependencies": {
|
package/src/tools/utils.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* whats-mcp ā Utility tools (6 tools).
|
|
3
3
|
*
|
|
4
4
|
* connection_status, whatsapp_guide, send_presence,
|
|
5
|
-
* read_messages, search_messages, download_media,
|
|
5
|
+
* read_messages, search_messages, download_media, cleanup_media,
|
|
6
6
|
* analytics_overview, analytics_top_chats, analytics_chat_insights,
|
|
7
7
|
* analytics_timeline, analytics_search
|
|
8
8
|
*/
|
|
@@ -240,7 +240,7 @@ module.exports = [
|
|
|
240
240
|
name: "download_media",
|
|
241
241
|
description:
|
|
242
242
|
"Download media (image, video, audio, document, sticker) from a message." +
|
|
243
|
-
" Returns the media
|
|
243
|
+
" Returns the local file path where the media was saved ($HOME/.cache/whats_media/)." +
|
|
244
244
|
" The message must be in the local store.",
|
|
245
245
|
inputSchema: {
|
|
246
246
|
type: "object",
|
|
@@ -291,17 +291,80 @@ module.exports = [
|
|
|
291
291
|
|
|
292
292
|
// Use Baileys downloadMediaMessage
|
|
293
293
|
const { downloadMediaMessage } = require("@whiskeysockets/baileys");
|
|
294
|
-
|
|
295
|
-
|
|
294
|
+
let buffer;
|
|
295
|
+
try {
|
|
296
|
+
buffer = await downloadMediaMessage(msg, "buffer", {});
|
|
297
|
+
} catch (err) {
|
|
298
|
+
return errResult(`Failed to download media: ${err.message}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const os = require("os");
|
|
302
|
+
const fs = require("fs");
|
|
303
|
+
const path = require("path");
|
|
304
|
+
|
|
305
|
+
const cacheDir = path.join(os.homedir(), ".cache", "whats_media");
|
|
306
|
+
if (!fs.existsSync(cacheDir)) {
|
|
307
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let fileName = mediaMsg.fileName || mediaMsg.title || `${message_id}.${mediaType}`;
|
|
311
|
+
fileName = fileName.replace(/[^a-zA-Z0-9.\-_]/g, "_");
|
|
312
|
+
|
|
313
|
+
let ext = path.extname(fileName);
|
|
314
|
+
if (!ext && mediaMsg.mimetype) {
|
|
315
|
+
const mimeExt = mediaMsg.mimetype.split("/")[1]?.split(";")[0];
|
|
316
|
+
if (mimeExt) fileName += `.${mimeExt}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const filePath = path.join(cacheDir, fileName);
|
|
320
|
+
fs.writeFileSync(filePath, buffer);
|
|
296
321
|
|
|
297
322
|
return okResult({
|
|
298
323
|
message_id,
|
|
299
324
|
media_type: mediaType,
|
|
300
325
|
mimetype: mediaMsg.mimetype || null,
|
|
301
|
-
filename:
|
|
302
|
-
file_length:
|
|
303
|
-
|
|
304
|
-
|
|
326
|
+
filename: fileName,
|
|
327
|
+
file_length: buffer.length,
|
|
328
|
+
saved_to: filePath,
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
// 7. cleanup_media
|
|
334
|
+
{
|
|
335
|
+
definition: {
|
|
336
|
+
name: "cleanup_media",
|
|
337
|
+
description: "Clear the local WhatsApp media cache directory ($HOME/.cache/whats_media/).",
|
|
338
|
+
inputSchema: { type: "object", properties: {} },
|
|
339
|
+
},
|
|
340
|
+
handler: async () => {
|
|
341
|
+
const os = require("os");
|
|
342
|
+
const fs = require("fs");
|
|
343
|
+
const path = require("path");
|
|
344
|
+
|
|
345
|
+
const cacheDir = path.join(os.homedir(), ".cache", "whats_media");
|
|
346
|
+
if (!fs.existsSync(cacheDir)) {
|
|
347
|
+
return okResult({ status: "skipped", message: "Cache directory does not exist." });
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let count = 0;
|
|
351
|
+
let bytesFreed = 0;
|
|
352
|
+
const files = fs.readdirSync(cacheDir);
|
|
353
|
+
for (const file of files) {
|
|
354
|
+
const filePath = path.join(cacheDir, file);
|
|
355
|
+
const stats = fs.statSync(filePath);
|
|
356
|
+
if (stats.isFile()) {
|
|
357
|
+
bytesFreed += stats.size;
|
|
358
|
+
fs.unlinkSync(filePath);
|
|
359
|
+
count++;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return okResult({
|
|
364
|
+
status: "cleaned",
|
|
365
|
+
files_deleted: count,
|
|
366
|
+
bytes_freed: bytesFreed,
|
|
367
|
+
cache_dir: cacheDir
|
|
305
368
|
});
|
|
306
369
|
},
|
|
307
370
|
},
|
|
@@ -313,7 +376,7 @@ function _guessCategory(name) {
|
|
|
313
376
|
if (/channel|newsletter/.test(name)) return "channels";
|
|
314
377
|
if (/label/.test(name)) return "labels";
|
|
315
378
|
if (/^analytics_|^daily_digest/.test(name)) return "analytics";
|
|
316
|
-
if (/^connection_status$|^whatsapp_guide$|^send_presence$|^read_messages$|^search_messages$|^download_media$/.test(name)) {
|
|
379
|
+
if (/^connection_status$|^whatsapp_guide$|^send_presence$|^read_messages$|^search_messages$|^download_media$|^cleanup_media$/.test(name)) {
|
|
317
380
|
return "utilities";
|
|
318
381
|
}
|
|
319
382
|
if (/^send_|^edit_|^delete_|^forward_|^batch_/.test(name)) return "messaging";
|