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 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
- - npm ci
10
- - npm test -- --runInBand
11
- - 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');"
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 "šŸš€ Deploying whats-mcp to the homelab..."
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 node -e "fetch('http://127.0.0.1:8092/health').then((r) => { if (!r.ok) process.exit(1); }).catch(() => process.exit(1))"
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
+ [![npm](https://img.shields.io/npm/v/whats-mcp)](https://www.npmjs.com/package/whats-mcp)
4
+ [![Node](https://img.shields.io/node/v/whats-mcp)](https://www.npmjs.com/package/whats-mcp)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
- npm install
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.1.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": "node src/main.js serve",
12
- "serve:http": "node src/main.js serve-http",
13
- "admin": "node src/admin.js",
14
- "login": "node src/admin.js login",
15
- "login:code": "node src/admin.js login --code",
16
- "test": "node --experimental-vm-modules node_modules/.bin/jest --coverage",
17
- "test:watch": "node --experimental-vm-modules node_modules/.bin/jest --watch",
18
- "stop": "node src/admin.js server stop",
19
- "status": "node src/admin.js status"
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": {
@@ -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 as base64-encoded data." +
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
- const buffer = await downloadMediaMessage(msg, "buffer", {});
295
- const base64 = buffer.toString("base64");
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: mediaMsg.fileName || null,
302
- file_length: mediaMsg.fileLength ? Number(mediaMsg.fileLength) : buffer.length,
303
- base64_length: base64.length,
304
- data: base64,
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";