wazap-mcp 0.9.5 → 0.9.6
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/README.md +98 -6
- package/dist/config.js +13 -0
- package/dist/connect.js +23 -2
- package/dist/index.js +7 -1
- package/dist/skills.js +90 -0
- package/package.json +8 -2
- package/skills/wazap-setup/SKILL.md +75 -0
- package/skills/whatsapp-groups/SKILL.md +47 -0
- package/skills/whatsapp-inbox/SKILL.md +40 -0
- package/skills/whatsapp-recall/SKILL.md +25 -0
- package/skills/whatsapp-send/SKILL.md +37 -0
package/README.md
CHANGED
|
@@ -25,6 +25,23 @@ npx wazap-mcp setup
|
|
|
25
25
|
That is the whole install. It links your account, finds the MCP clients
|
|
26
26
|
installed on this machine, writes their config and tells you what to restart.
|
|
27
27
|
|
|
28
|
+
### Or the path your harness prefers
|
|
29
|
+
|
|
30
|
+
| Harness | Fastest path |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| Claude Code | `/plugin marketplace add razvangirgiz/wazap`, then `/plugin install wazap@wazap` |
|
|
33
|
+
| Claude Desktop | download `wazap-<version>.mcpb` from [Releases](https://github.com/razvangirgiz/wazap/releases) and double-click it |
|
|
34
|
+
| Gemini CLI | `gemini extensions install https://github.com/razvangirgiz/wazap` |
|
|
35
|
+
| Cursor | the [Install in Cursor](#other-mcp-clients) badge, then `npx wazap-mcp skills install cursor` |
|
|
36
|
+
| VS Code | the [Install in VS Code](#other-mcp-clients) badge |
|
|
37
|
+
| Codex CLI | `npx wazap-mcp connect codex`, then `npx wazap-mcp skills install codex` |
|
|
38
|
+
| OpenCode | `npx wazap-mcp connect opencode`, then `npx wazap-mcp skills install opencode` |
|
|
39
|
+
| Windsurf | `npx wazap-mcp connect windsurf` |
|
|
40
|
+
| Anything else | the MCP entry `npx -y wazap-mcp` over stdio, or a [self-hosted](#self-host) URL |
|
|
41
|
+
|
|
42
|
+
Each of those registers the server. Linking the WhatsApp account is a separate,
|
|
43
|
+
one-time step in every one of them: `npx wazap-mcp login`.
|
|
44
|
+
|
|
28
45
|
Or have your agent do it. Paste this:
|
|
29
46
|
|
|
30
47
|
*Set up WhatsApp for me: run `npx wazap-mcp setup --agent` and follow what it prints.*
|
|
@@ -63,11 +80,25 @@ it would write.
|
|
|
63
80
|
| `codex` | `[mcp_servers.whatsapp]` in `~/.codex/config.toml` |
|
|
64
81
|
| `vscode` | `./.vscode/mcp.json`, for the current workspace |
|
|
65
82
|
| `gemini` | `~/.gemini/settings.json` |
|
|
83
|
+
| `windsurf` | `~/.codeium/windsurf/mcp_config.json` |
|
|
84
|
+
| `opencode` | `mcp.whatsapp` in `~/.config/opencode/opencode.json` |
|
|
66
85
|
| anything remote | client's MCP URL field: `https://your-host/mcp` with header `Authorization: Bearer <token>` (see [Self-host](#self-host)) |
|
|
67
86
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
### Other MCP clients
|
|
88
|
+
|
|
89
|
+
Cursor and VS Code install from a link:
|
|
90
|
+
|
|
91
|
+
[](cursor://anysphere.cursor-deeplink/mcp/install?name=whatsapp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIndhemFwLW1jcCJdfQ)
|
|
92
|
+
[](vscode:mcp/install?%7B%22name%22%3A%22whatsapp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22wazap-mcp%22%5D%7D)
|
|
93
|
+
|
|
94
|
+
Both carry the same entry `connect` writes. Where a custom scheme is stripped
|
|
95
|
+
before you can click it, VS Code also takes
|
|
96
|
+
[the https form](https://insiders.vscode.dev/redirect/mcp/install?name=whatsapp&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22wazap-mcp%22%5D%7D).
|
|
97
|
+
`node scripts/badges.mjs` reprints all three.
|
|
98
|
+
|
|
99
|
+
Any other MCP client works the same way: the command is `npx -y wazap-mcp`, the
|
|
100
|
+
transport is stdio. Tell the agent to call `learn` first — it returns the id
|
|
101
|
+
formats, the workflows and every error code with what to do about it.
|
|
71
102
|
|
|
72
103
|
<details>
|
|
73
104
|
<summary>The raw entries, for editing by hand</summary>
|
|
@@ -83,8 +114,9 @@ workflows and every error code with what to do about it.
|
|
|
83
114
|
}
|
|
84
115
|
```
|
|
85
116
|
|
|
86
|
-
Claude Desktop, Cursor
|
|
87
|
-
`servers` and wants a `"type": "stdio"` alongside `command`. Codex CLI
|
|
117
|
+
Claude Desktop, Cursor, Gemini CLI and Windsurf take exactly that. VS Code nests
|
|
118
|
+
it under `servers` and wants a `"type": "stdio"` alongside `command`. Codex CLI
|
|
119
|
+
is TOML:
|
|
88
120
|
|
|
89
121
|
```toml
|
|
90
122
|
[mcp_servers.whatsapp]
|
|
@@ -92,10 +124,55 @@ command = "npx"
|
|
|
92
124
|
args = ["-y", "wazap-mcp"]
|
|
93
125
|
```
|
|
94
126
|
|
|
127
|
+
OpenCode takes the command and its arguments as one array, under `mcp`:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"mcp": {
|
|
132
|
+
"whatsapp": { "type": "local", "command": ["npx", "-y", "wazap-mcp"] }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
95
137
|
</details>
|
|
96
138
|
|
|
97
139
|
The `skills/` folder follows the [Agent Skills](https://agentskills.io) format, so Codex, Cursor and other skill-aware agents can load the same five skills.
|
|
98
140
|
|
|
141
|
+
### Gemini CLI
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
gemini extensions install https://github.com/razvangirgiz/wazap
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
That reads `gemini-extension.json` at the repo root, so it registers the MCP
|
|
148
|
+
server and loads `GEMINI.md` — the five skills below, concatenated, because the
|
|
149
|
+
Gemini CLI takes one context file per extension rather than a skills directory.
|
|
150
|
+
`wazap connect gemini` writes the server alone, without the context.
|
|
151
|
+
|
|
152
|
+
`GEMINI.md` is generated: `npm run context:build` rebuilds it from
|
|
153
|
+
`skills/*/SKILL.md`, and a test fails if the two have drifted, so a workflow is
|
|
154
|
+
only ever edited in its skill.
|
|
155
|
+
|
|
156
|
+
### Claude Desktop, without a terminal
|
|
157
|
+
|
|
158
|
+
Download `wazap-<version>.mcpb` from [Releases](https://github.com/razvangirgiz/wazap/releases)
|
|
159
|
+
and double-click it. Claude Desktop installs the server, its Node dependencies
|
|
160
|
+
and the icon, and shows two settings: **Read-only**, ticked, and **Data
|
|
161
|
+
directory**, empty. `wazap connect claude-desktop` does the same job by editing
|
|
162
|
+
`claude_desktop_config.json`, and needs `npx` at launch; the bundle does not.
|
|
163
|
+
|
|
164
|
+
Linking the account still needs a terminal once: `npx wazap-mcp login`. The
|
|
165
|
+
bundle reads the session that login writes to `~/.wazap`.
|
|
166
|
+
|
|
167
|
+
Untick **Read-only** to let Claude send. It ships ticked because a bundle that
|
|
168
|
+
can message people from your number before you have said so is the wrong
|
|
169
|
+
default, and because the setting cannot be left unanswered: the manifest format
|
|
170
|
+
has no way to omit an argument, so the box you see is the answer the server gets.
|
|
171
|
+
|
|
172
|
+
Build it yourself with `npm run bundle:mcpb`, which stages `dist/`, the
|
|
173
|
+
manifest, the icon and a fresh production `node_modules`, then packs them with
|
|
174
|
+
[`@anthropic-ai/mcpb`](https://github.com/modelcontextprotocol/mcpb).
|
|
175
|
+
|
|
99
176
|
## Tools
|
|
100
177
|
|
|
101
178
|
| Tool | Kind | What it does |
|
|
@@ -148,7 +225,22 @@ Install everything (server and skills) as a Claude Code plugin:
|
|
|
148
225
|
/plugin install wazap@wazap
|
|
149
226
|
```
|
|
150
227
|
|
|
151
|
-
|
|
228
|
+
Every other harness gets them with one command:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
npx wazap-mcp skills install codex # or cursor, opencode, agents
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
| Harness | Where the five directories land |
|
|
235
|
+
| --- | --- |
|
|
236
|
+
| `claude-code` | nowhere — the plugin above already carries them |
|
|
237
|
+
| `codex` | `~/.agents/skills/`, the directory Codex documents for user skills. Cursor and OpenCode read it too |
|
|
238
|
+
| `cursor` | `~/.cursor/skills/` |
|
|
239
|
+
| `opencode` | `~/.config/opencode/skills/` |
|
|
240
|
+
| `agents` | `./.agents/skills/`, in the current project, for anything that reads the cross-tool convention |
|
|
241
|
+
|
|
242
|
+
Re-running overwrites, so an upgrade is the same command. `--dry-run` lists
|
|
243
|
+
what it would copy.
|
|
152
244
|
|
|
153
245
|
## Errors
|
|
154
246
|
|
package/dist/config.js
CHANGED
|
@@ -30,6 +30,7 @@ const COMMAND_ARGS = {
|
|
|
30
30
|
connect: [1],
|
|
31
31
|
config: [0, 2],
|
|
32
32
|
contacts: [1],
|
|
33
|
+
skills: [2],
|
|
33
34
|
};
|
|
34
35
|
const COMMANDS = Object.keys(COMMAND_ARGS);
|
|
35
36
|
export function defaultDataDir() {
|
|
@@ -44,6 +45,17 @@ function asInt(value, fallback) {
|
|
|
44
45
|
const n = Number.parseInt((value ?? "").trim(), 10);
|
|
45
46
|
return Number.isFinite(n) ? n : fallback;
|
|
46
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* A user_config slot the person never filled in reaches us as the literal
|
|
50
|
+
* `${user_config.data_dir}`: the Claude Desktop bundle substitutes what it has
|
|
51
|
+
* and leaves the rest alone. An unanswered question is not a data directory.
|
|
52
|
+
*/
|
|
53
|
+
function dropUnfilledTemplates() {
|
|
54
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
55
|
+
if (key.startsWith("WAZAP_") && /^\$\{[^}]*\}$/.test(value ?? ""))
|
|
56
|
+
delete process.env[key];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
47
59
|
/**
|
|
48
60
|
* A human at a terminal running bare `wazap` wants to see where they stand, not
|
|
49
61
|
* a silent MCP server on stdin. Everything else serves, including `wazap serve`.
|
|
@@ -88,6 +100,7 @@ export function parseCli(argv = process.argv.slice(2)) {
|
|
|
88
100
|
return { kind: "help" };
|
|
89
101
|
if (values.version)
|
|
90
102
|
return { kind: "version" };
|
|
103
|
+
dropUnfilledTemplates();
|
|
91
104
|
const [first, ...args] = positionals;
|
|
92
105
|
if (first !== undefined && !COMMANDS.includes(first)) {
|
|
93
106
|
throw new WazapError("INVALID_ID", `Unknown command "${first}".`, "Run `wazap --help`");
|
package/dist/connect.js
CHANGED
|
@@ -58,7 +58,7 @@ export const CLIENTS = [
|
|
|
58
58
|
file: () => join(process.cwd(), ".vscode", "mcp.json"),
|
|
59
59
|
format: "json",
|
|
60
60
|
keyPath: ["servers", "whatsapp"],
|
|
61
|
-
|
|
61
|
+
value: (entry) => ({ type: "stdio", ...entry }),
|
|
62
62
|
next: "Written to ./.vscode/mcp.json for this workspace. Reload the VS Code window.",
|
|
63
63
|
detect: (probe) => probe.onPath("code"),
|
|
64
64
|
},
|
|
@@ -71,6 +71,27 @@ export const CLIENTS = [
|
|
|
71
71
|
next: "Restart the Gemini CLI.",
|
|
72
72
|
detect: (probe) => probe.exists(join(homedir(), ".gemini")),
|
|
73
73
|
},
|
|
74
|
+
{
|
|
75
|
+
name: "windsurf",
|
|
76
|
+
describe: "Windsurf",
|
|
77
|
+
file: () => join(homedir(), ".codeium", "windsurf", "mcp_config.json"),
|
|
78
|
+
format: "json",
|
|
79
|
+
keyPath: ["mcpServers", "whatsapp"],
|
|
80
|
+
next: "Refresh the MCP servers in Windsurf's Cascade panel.",
|
|
81
|
+
detect: (probe) => probe.exists(join(homedir(), ".codeium", "windsurf")),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "opencode",
|
|
85
|
+
describe: "OpenCode",
|
|
86
|
+
file: () => join(homedir(), ".config", "opencode", "opencode.json"),
|
|
87
|
+
format: "json",
|
|
88
|
+
keyPath: ["mcp", "whatsapp"],
|
|
89
|
+
// OpenCode takes one array where the others take a command and its args,
|
|
90
|
+
// and its schema refuses anything else under the key.
|
|
91
|
+
value: (entry) => ({ type: "local", command: [entry.command, ...entry.args] }),
|
|
92
|
+
next: "Restart OpenCode.",
|
|
93
|
+
detect: (probe) => probe.exists(join(homedir(), ".config", "opencode")),
|
|
94
|
+
},
|
|
74
95
|
];
|
|
75
96
|
export const CLIENT_NAMES = CLIENTS.map((client) => client.name).join(", ");
|
|
76
97
|
const OTHER_CLIENTS = CLIENTS.slice(1)
|
|
@@ -173,7 +194,7 @@ function writeJsonEntry(spec, entry, dryRun) {
|
|
|
173
194
|
}
|
|
174
195
|
doc = parsed;
|
|
175
196
|
}
|
|
176
|
-
const value =
|
|
197
|
+
const value = spec.value?.(entry) ?? { command: entry.command, args: entry.args };
|
|
177
198
|
setIn(doc, spec.keyPath, value);
|
|
178
199
|
// Indent 1 collapsed to one line: short enough to read, still spaced like JSON.
|
|
179
200
|
const shown = JSON.stringify(value, null, 1).replace(/\n\s*/g, " ");
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { BANNER } from "./banner.js";
|
|
|
3
3
|
import { runContacts, runGreet, runLogin, runLogout, runServe, runStatus } from "./cli.js";
|
|
4
4
|
import { WAZAP_VERSION, parseCli, pickDefaultAction } from "./config.js";
|
|
5
5
|
import { CLIENT_NAMES, runConnect } from "./connect.js";
|
|
6
|
+
import { SKILL_TARGET_NAMES, runSkills } from "./skills.js";
|
|
6
7
|
import { runSetup } from "./setup.js";
|
|
7
8
|
import { runConfig } from "./settings.js";
|
|
8
9
|
import { WazapError } from "./errors.js";
|
|
@@ -15,12 +16,14 @@ Usage:
|
|
|
15
16
|
wazap login [--phone +15550100] [--code] Link a WhatsApp account (QR by default)
|
|
16
17
|
wazap setup [--agent] [--client <name>] Link, connect your client and finish, in one command
|
|
17
18
|
wazap connect <client> [--dry-run] Register wazap with an MCP client
|
|
19
|
+
wazap skills install <harness> [--dry-run] Copy the five skills into a harness
|
|
18
20
|
wazap config [writes on|off] Show the effective settings, or allow/refuse writes
|
|
19
21
|
wazap contacts resync Fetch the phone's address book from WhatsApp again
|
|
20
22
|
wazap status [--live] [--json] Check the install, the session and the server
|
|
21
23
|
wazap logout Unlink and delete local credentials
|
|
22
24
|
|
|
23
25
|
Clients for wazap connect: ${CLIENT_NAMES}.
|
|
26
|
+
Harnesses for wazap skills install: ${SKILL_TARGET_NAMES}.
|
|
24
27
|
|
|
25
28
|
Options:
|
|
26
29
|
--data-dir <path> Where wazap keeps its data (default ~/.wazap, or $WAZAP_DATA_DIR)
|
|
@@ -32,7 +35,7 @@ Options:
|
|
|
32
35
|
--phone <number> Your number in international format; implies --code
|
|
33
36
|
--agent With setup: print the procedure for an AI agent on stdout, then exit
|
|
34
37
|
--client <name> With setup: connect this client instead of the detected ones (repeatable)
|
|
35
|
-
--dry-run With connect: print what would be written, and write nothing
|
|
38
|
+
--dry-run With connect or skills install: print what would be written, and write nothing
|
|
36
39
|
--live With status: reach WhatsApp for real, then close the connection
|
|
37
40
|
--json With status: print the whole report as one JSON object on stdout
|
|
38
41
|
--writes Allow the agent to write, without login asking
|
|
@@ -73,6 +76,9 @@ async function main() {
|
|
|
73
76
|
case "connect":
|
|
74
77
|
runConnect(config);
|
|
75
78
|
return;
|
|
79
|
+
case "skills":
|
|
80
|
+
runSkills(config);
|
|
81
|
+
return;
|
|
76
82
|
case "config":
|
|
77
83
|
runConfig(config);
|
|
78
84
|
return;
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { cpSync, readdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { WazapError } from "./errors.js";
|
|
6
|
+
import { say } from "./logger.js";
|
|
7
|
+
import { info, next, nextHint, ok, shortPath } from "./ui.js";
|
|
8
|
+
/**
|
|
9
|
+
* Each path is the one that harness's own docs name today. Codex is the
|
|
10
|
+
* one that moved: `~/.codex/skills` still loads, but its docs call that
|
|
11
|
+
* location deprecated and put user skills in `~/.agents/skills`, which Cursor
|
|
12
|
+
* and OpenCode read too.
|
|
13
|
+
*/
|
|
14
|
+
export const SKILL_TARGETS = [
|
|
15
|
+
{
|
|
16
|
+
name: "claude-code",
|
|
17
|
+
describe: "Claude Code",
|
|
18
|
+
dir: null,
|
|
19
|
+
next: "/plugin marketplace add razvangirgiz/wazap",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "codex",
|
|
23
|
+
describe: "Codex CLI",
|
|
24
|
+
dir: () => join(homedir(), ".agents", "skills"),
|
|
25
|
+
next: "Restart Codex. Cursor and OpenCode read this directory too.",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "cursor",
|
|
29
|
+
describe: "Cursor",
|
|
30
|
+
dir: () => join(homedir(), ".cursor", "skills"),
|
|
31
|
+
next: "Reload the Cursor window.",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "opencode",
|
|
35
|
+
describe: "OpenCode",
|
|
36
|
+
dir: () => join(homedir(), ".config", "opencode", "skills"),
|
|
37
|
+
next: "Restart OpenCode.",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "agents",
|
|
41
|
+
describe: "This project, any agent",
|
|
42
|
+
dir: () => join(process.cwd(), ".agents", "skills"),
|
|
43
|
+
next: "Commit .agents/skills to share them with whoever clones this repo.",
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
export const SKILL_TARGET_NAMES = SKILL_TARGETS.map((target) => target.name).join(", ");
|
|
47
|
+
/** The `skills/` folder shipped in the npm package, next to `dist/`. */
|
|
48
|
+
function packagedSkills() {
|
|
49
|
+
return fileURLToPath(new URL("../skills", import.meta.url));
|
|
50
|
+
}
|
|
51
|
+
function skillNames() {
|
|
52
|
+
try {
|
|
53
|
+
return readdirSync(packagedSkills(), { withFileTypes: true })
|
|
54
|
+
.filter((entry) => entry.isDirectory())
|
|
55
|
+
.map((entry) => entry.name)
|
|
56
|
+
.sort();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw new WazapError("FILE_NOT_FOUND", "This wazap install ships no skills/ directory.", "Upgrade with `npm i -g wazap-mcp@latest`, or run this from a checkout.");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function findSkillTarget(name) {
|
|
63
|
+
const target = SKILL_TARGETS.find((candidate) => candidate.name === name);
|
|
64
|
+
if (!target) {
|
|
65
|
+
throw new WazapError("INVALID_ID", `Unknown harness "${name}".`, `Pick one of: ${SKILL_TARGET_NAMES}`);
|
|
66
|
+
}
|
|
67
|
+
return target;
|
|
68
|
+
}
|
|
69
|
+
export function runSkills(config) {
|
|
70
|
+
if (config.args[0] !== "install") {
|
|
71
|
+
throw new WazapError("INVALID_ID", `Unknown skills command "${config.args[0]}".`, "Run `wazap skills install <harness>`");
|
|
72
|
+
}
|
|
73
|
+
const target = findSkillTarget(config.args[1] ?? "");
|
|
74
|
+
if (target.dir === null) {
|
|
75
|
+
say(info(`${target.describe} loads these skills from the wazap plugin, along with the MCP server.`));
|
|
76
|
+
say(next(target.next));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const dir = target.dir();
|
|
80
|
+
const names = skillNames();
|
|
81
|
+
say(info(`${target.describe} · ${config.dryRun ? "would copy into" : "copying into"} ${shortPath(dir)}`));
|
|
82
|
+
for (const name of names) {
|
|
83
|
+
// Overwriting is the point: this is how an upgrade reaches an already
|
|
84
|
+
// installed harness, and re-running it must land in the same place.
|
|
85
|
+
if (!config.dryRun)
|
|
86
|
+
cpSync(join(packagedSkills(), name), join(dir, name), { recursive: true, force: true });
|
|
87
|
+
say(` ${ok(name)}`);
|
|
88
|
+
}
|
|
89
|
+
say(nextHint(target.next));
|
|
90
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wazap-mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
|
+
"mcpName": "io.github.razvangirgiz/wazap",
|
|
4
5
|
"description": "WhatsApp for your AI agent. MCP server over Baileys: pairing-code login, 22 tools, stdio or token-protected HTTP.",
|
|
5
6
|
"license": "MIT",
|
|
6
7
|
"author": "Răzvan Girgiz",
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
},
|
|
27
28
|
"files": [
|
|
28
29
|
"dist",
|
|
30
|
+
"skills",
|
|
29
31
|
"README.md",
|
|
30
32
|
"AGENT.md",
|
|
31
33
|
"LICENSE"
|
|
@@ -36,7 +38,11 @@
|
|
|
36
38
|
"dev": "tsx src/index.ts",
|
|
37
39
|
"typecheck": "tsc --noEmit",
|
|
38
40
|
"test": "npm run build && node --test test/*.test.mjs",
|
|
39
|
-
"prepublishOnly": "npm run build && npm test"
|
|
41
|
+
"prepublishOnly": "npm run build && npm test",
|
|
42
|
+
"registry:validate": "node scripts/validate-server-json.mjs",
|
|
43
|
+
"bundle:mcpb": "node scripts/bundle-mcpb.mjs",
|
|
44
|
+
"icon": "node scripts/make-icon.mjs",
|
|
45
|
+
"context:build": "node scripts/build-context.mjs"
|
|
40
46
|
},
|
|
41
47
|
"engines": {
|
|
42
48
|
"node": ">=20"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wazap-setup
|
|
3
|
+
description: Install, link or repair the WhatsApp connection. Use when the user wants to connect WhatsApp to Claude, when a WhatsApp tool returns NOT_LINKED, SESSION_EXPIRED or SESSION_CORRUPT, when `get_status` is anything but connected, or when the user asks how wazap works with Claude Desktop, Claude Code or HTTP.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# wazap setup
|
|
7
|
+
|
|
8
|
+
wazap links the user's own WhatsApp account as a "linked device" and exposes it as MCP tools. The phone must stay online; the link needs the user's hands once.
|
|
9
|
+
|
|
10
|
+
## Diagnose first
|
|
11
|
+
|
|
12
|
+
Run `npx wazap-mcp status` and branch on its output. It never contacts WhatsApp, so it is safe at any point.
|
|
13
|
+
|
|
14
|
+
| `status` says | Do |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `wazap: command not found` / npx fails | Node 20+ is required. `node --version`; install from nodejs.org if older. |
|
|
17
|
+
| `linked: no` | Go to **Link**. |
|
|
18
|
+
| `linked: yes`, `server: running` | The server is up. If tools still fail, call `get_status` and follow its `fix`. |
|
|
19
|
+
| `linked: yes`, `server: not running` | Go to **Connect a client**. |
|
|
20
|
+
|
|
21
|
+
Below those lines is a `checks:` section. Every `✗` carries the command that
|
|
22
|
+
fixes it; run that command rather than improvising.
|
|
23
|
+
|
|
24
|
+
| `checks:` line | What it means |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `✗ node` | The Node version is below 20. Nothing else will work until it is upgraded. |
|
|
27
|
+
| `✗ data dir` | Missing, not a directory, mode other than 0700, or not writable. The line names the `chmod` to run. |
|
|
28
|
+
| `– lock: stale` | A previous server died without cleaning up. Harmless; the next start reclaims it. |
|
|
29
|
+
| `✓ lock: held` | A server is running. Do not run `logout` or `status --live`; ask through the client with `get_status`. |
|
|
30
|
+
| `✗ credentials` | Unreadable. `npx wazap-mcp logout` then `npx wazap-mcp login`. |
|
|
31
|
+
| `writes: off` | Write tools are not registered. Enabling them is **Allow writes**. |
|
|
32
|
+
| `– update` | A newer wazap exists, or the check could not reach npm. Never blocking. |
|
|
33
|
+
|
|
34
|
+
`npx wazap-mcp status --live` reaches WhatsApp for real and reports whether the
|
|
35
|
+
phone is reachable, how many chats synced and how old the last message is. It
|
|
36
|
+
refuses while a server holds the lock, because one process owns the session.
|
|
37
|
+
`--json` gives the same report as one object.
|
|
38
|
+
|
|
39
|
+
## Link
|
|
40
|
+
|
|
41
|
+
Run `npx wazap-mcp setup --agent` and follow what it prints.
|
|
42
|
+
|
|
43
|
+
That procedure starts `login` in the background and reads the
|
|
44
|
+
`pairing code: XXXX-XXXX` line out of its output, so the user is left with the
|
|
45
|
+
one part of linking a machine cannot do, typing the code into the phone.
|
|
46
|
+
|
|
47
|
+
`SESSION_EXPIRED` means the phone removed the device: run `npx wazap-mcp login` again. `SESSION_CORRUPT` means unreadable credentials: `npx wazap-mcp logout` then `npx wazap-mcp login`.
|
|
48
|
+
|
|
49
|
+
## Connect a client
|
|
50
|
+
|
|
51
|
+
Run `npx wazap-mcp connect <client>`, where the client is one of `claude-code`,
|
|
52
|
+
`claude-desktop`, `cursor`, `codex`, `vscode` or `gemini`. It writes the entry,
|
|
53
|
+
keeps whatever else is in the file, backs it up once, and prints the next step
|
|
54
|
+
(restart, reload window, or `claude mcp list`). Running it twice is safe.
|
|
55
|
+
|
|
56
|
+
- Add `--dry-run` first if the user wants to see the entry before it is written.
|
|
57
|
+
- Claude Code users can install this plugin instead, which registers the server.
|
|
58
|
+
- Remote clients (claude.ai, another machine) need HTTP mode with tokens; follow "HTTP mode" in the wazap README rather than improvising.
|
|
59
|
+
|
|
60
|
+
Done when `get_status` returns `status: "connected"`. Then call `learn` once before using the other tools.
|
|
61
|
+
|
|
62
|
+
## Allow writes
|
|
63
|
+
|
|
64
|
+
Writes are off unless the user said yes at `login`. The write tools are then not
|
|
65
|
+
registered at all, so the agent cannot see them.
|
|
66
|
+
|
|
67
|
+
Turn them on with `npx wazap-mcp config writes on`, off again with
|
|
68
|
+
`npx wazap-mcp config writes off`. Both edit `WAZAP_READ_ONLY` in
|
|
69
|
+
`<data-dir>/.env`; a running server has to be restarted for the change to take
|
|
70
|
+
effect. `npx wazap-mcp config` alone prints every effective setting and where it
|
|
71
|
+
came from, which is how you tell a flag from an `.env` line.
|
|
72
|
+
|
|
73
|
+
## Limits the user should hear once
|
|
74
|
+
|
|
75
|
+
Baileys is an unofficial WhatsApp client; Meta can flag accounts, so heavy automated sending is the user's risk. One process owns the session: two wazap servers on the same data dir refuse to start.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: whatsapp-groups
|
|
3
|
+
description: Catch up on a busy WhatsApp group. Use when the user asks what happened in a group, what was decided, what they are being asked to do there, or for a summary of a thread with many messages. Read-only.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# WhatsApp group catch-up
|
|
7
|
+
|
|
8
|
+
Deliverable: decisions, dates, and what is asked of the user, in that order, with the chatter gone. A 300-message thread should compress to a screen.
|
|
9
|
+
|
|
10
|
+
## Load the whole window
|
|
11
|
+
|
|
12
|
+
1. Resolve the group with `list_chats` `filter: "groups"` (match on name; ask if two match). `get_group_info` once for the participant names and who the admins are; use names, not numbers, in the summary.
|
|
13
|
+
2. `read_messages` with `limit: 200`. If the oldest message is still inside the window the user asked for, call again with `before` set to that oldest `message_id`, until the window is covered. Done loading when the oldest message you hold is older than the window, or WhatsApp returns no more.
|
|
14
|
+
3. Note which messages quote or mention the user: `quoted.sender` equal to the user, the user's name in `text`, or `sender` addressing them directly. These are the **asks**.
|
|
15
|
+
|
|
16
|
+
## Extract
|
|
17
|
+
|
|
18
|
+
Work through the messages once and collect:
|
|
19
|
+
|
|
20
|
+
- **Decisions**: something agreed or announced by an admin or by the people it concerns ("ok, Saturday at 10 then").
|
|
21
|
+
- **Dates and deadlines**: any concrete day, time, or "by Friday", with what it is for.
|
|
22
|
+
- **Asks of the user**: every mention or reply to them, plus open questions nobody answered that fall on the user.
|
|
23
|
+
- **Open threads**: questions still without an answer, for anyone.
|
|
24
|
+
- **Polls**: the question and options (`[poll] …`); wazap cannot read votes, so say that if the user asks who voted.
|
|
25
|
+
|
|
26
|
+
Skip greetings, reactions, stickers, and messages that only acknowledge.
|
|
27
|
+
|
|
28
|
+
## Report
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Bloc 12 — 312 messages since Monday
|
|
32
|
+
|
|
33
|
+
Decided
|
|
34
|
+
- Roof repair goes to Tehnoplast, 18,400 lei, vote closed Wednesday.
|
|
35
|
+
- Water off Thursday 09:00–13:00.
|
|
36
|
+
|
|
37
|
+
Dates
|
|
38
|
+
- Fri 15 Mar: pay share (1,150 lei) to the association account.
|
|
39
|
+
|
|
40
|
+
You
|
|
41
|
+
- Mihai asked (Tue) if you can be home Thursday for the plumber. Unanswered.
|
|
42
|
+
|
|
43
|
+
Open
|
|
44
|
+
- Nobody confirmed who holds the basement key.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
End with the message count and the window covered, so the user knows what the summary stands on. Replying in the group is the `whatsapp-send` skill's job; here, offer it only for the *You* items.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: whatsapp-inbox
|
|
3
|
+
description: Triage the user's WhatsApp. Use when they ask what they missed, what's unread, who is waiting on a reply, what needs attention today, or whom they forgot to answer. Read-only; it sends nothing and marks nothing read.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# WhatsApp inbox triage
|
|
7
|
+
|
|
8
|
+
Deliverable: a short, ranked list of what needs the user, with everything else compressed to one line. The user should finish reading in under a minute.
|
|
9
|
+
|
|
10
|
+
## Collect
|
|
11
|
+
|
|
12
|
+
1. `get_recent_messages` with the window the user implied (default 24h; "this week" = 168). If the result says `sync: "in_progress"`, wait 5 seconds and call it again once.
|
|
13
|
+
2. `list_chats` with `filter: "unread"` to catch chats whose activity predates the window.
|
|
14
|
+
3. For follow-ups ("whom did I forget"): in `list_chats` results, an individual chat whose `last_message.from_me` is false and older than 2 days is an unanswered conversation. Read its last 5 messages with `read_messages` to confirm something was actually asked.
|
|
15
|
+
|
|
16
|
+
Done collecting when every chat with unread messages appears in exactly one bucket below.
|
|
17
|
+
|
|
18
|
+
## Triage
|
|
19
|
+
|
|
20
|
+
Sort each chat into one bucket:
|
|
21
|
+
|
|
22
|
+
- **Needs you**: a direct question to the user, a request, a mention of the user in a group (`sender` is not the user and the text addresses them or quotes one of their messages), or money/dates/decisions awaiting them.
|
|
23
|
+
- **FYI**: information with no ask. Shipping updates, "ok thanks", group chatter that reached a conclusion.
|
|
24
|
+
- **Noise**: promotions, broadcast lists, groups the user is muted in (`muted_until` in the future), forwards without a question.
|
|
25
|
+
|
|
26
|
+
Rank *Needs you* by: people over groups, older unanswered over newer, money and deadlines first.
|
|
27
|
+
|
|
28
|
+
## Report
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Needs you (3)
|
|
32
|
+
1. Ana — asks if Thursday 10:00 works for the notary. 5h ago.
|
|
33
|
+
2. Bloc 12 group — Mihai needs your vote on the roof quote by Friday. 1d ago.
|
|
34
|
+
3. Dan — sent the contract PDF, waiting for your comments. 2d ago.
|
|
35
|
+
|
|
36
|
+
FYI: Curier (delivered), Mama (photos), Team (retro moved to Tuesday).
|
|
37
|
+
Noise: 4 promo chats.
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
One line per item: who, what they want, how old. Include the `chat_id` only if the user is likely to act through another tool next. Offer to draft replies only for *Needs you* items; drafting and sending belong to the `whatsapp-send` skill.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: whatsapp-recall
|
|
3
|
+
description: Find something in the user's WhatsApp history. Use when they ask for an address, invoice, photo, document, link, phone number, date or a thing someone said or sent, or "what did X say about Y". Read-only.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# WhatsApp recall
|
|
7
|
+
|
|
8
|
+
Deliverable: the exact message or file, quoted with who sent it and when, or a clear "not found" that says where you looked.
|
|
9
|
+
|
|
10
|
+
## Narrow, then search
|
|
11
|
+
|
|
12
|
+
1. If a person or group is named, resolve it with `search_contacts` (people) or `list_chats` with `filter: "groups"` (groups) and search inside that `chat_id`. Searching one chat beats searching everything.
|
|
13
|
+
2. `search_messages` with the user's words. If nothing matches, run two more variants before giving up: a synonym or the other language the user writes in, and a narrower fragment (a street name instead of "the address", "factura" instead of "the invoice from March").
|
|
14
|
+
3. Media has no searchable text beyond its caption and the `[image]`/`[document] name.pdf` placeholder. For "the photo of…" or "the PDF", search the placeholder and filename words, or `read_messages` on the chat with `limit: 100` and scan `type` and `media.filename`.
|
|
15
|
+
4. Older than what is loaded: `read_messages` with `before` set to the oldest `message_id` you have, repeatedly, until the date the user remembers is covered or WhatsApp returns nothing more.
|
|
16
|
+
|
|
17
|
+
Done searching when you have a match, or all three query variants and the pagination step came back empty.
|
|
18
|
+
|
|
19
|
+
## Deliver
|
|
20
|
+
|
|
21
|
+
- A message: quote the text verbatim, then sender, chat, timestamp. Example: *"Str. Lunii 14, ap. 3, interfon 31" — Ana, 12 Mar 14:05.*
|
|
22
|
+
- A file or photo: `download_media` on the message, then open the saved file with your file-reading tool and answer from its contents when the user asked a question about it (an amount, a clause, a date). Give the saved path so the user can open it.
|
|
23
|
+
- A link: return the URL as sent; do not fetch it unless asked.
|
|
24
|
+
- Several candidates: list up to 5 with sender and date and ask which one, rather than guessing.
|
|
25
|
+
- Not found: say which chats and which phrases you tried, and whether `MEDIA_UNAVAILABLE` blocked a download (the sender must resend).
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: whatsapp-send
|
|
3
|
+
description: Send, reply, forward, share a file, react, or create a poll on WhatsApp on the user's behalf. Use for any request that results in an outgoing WhatsApp message. Drafts first and sends after the user approves; messages leave from the user's own account and cannot be unsent.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# WhatsApp send
|
|
7
|
+
|
|
8
|
+
A message sent here is indistinguishable from one the user typed. The rail: **the user sees recipient and exact text, says yes, then it goes.** One approval covers one message to one chat.
|
|
9
|
+
|
|
10
|
+
## Resolve the recipient
|
|
11
|
+
|
|
12
|
+
1. `search_contacts` with the name. Exactly one match: use its `chat_id`. Several: list them with numbers and ask. None: ask for the number in international format; `NOT_ON_WHATSAPP` means the number is wrong, not that you should retry.
|
|
13
|
+
2. Groups come from `list_chats` with `filter: "groups"`. Before posting, `get_group_info`; if `announcement_only` is true and the user is not admin, say so instead of trying.
|
|
14
|
+
3. A reply to a specific message needs its `message_id` from `read_messages`; pass it as `reply_to` so the quote shows.
|
|
15
|
+
|
|
16
|
+
## Draft
|
|
17
|
+
|
|
18
|
+
1. `read_messages` on the chat, `limit: 20`, and match the register already in use: language (Romanian or English), formality, emoji, length. A two-line chat gets a two-line reply.
|
|
19
|
+
2. Write the message as the user, first person, without a signature or "sent by an assistant".
|
|
20
|
+
3. Files: `send_media` needs a local `file_path` that exists on the machine running wazap, or a public URL. Check the path before drafting; pick `as_document: true` for PDFs and anything the recipient should keep at original quality, `as_voice: true` only for audio meant as a voice note.
|
|
21
|
+
|
|
22
|
+
## Confirm, then send
|
|
23
|
+
|
|
24
|
+
Show exactly this and wait for a yes:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
To: Ana (+40 722 …)
|
|
28
|
+
"Joi la 10 e perfect, ne vedem la notar. Aduc eu actele."
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Send on the user's yes with `send_message` / `send_media` / `send_poll` / `send_location` / `forward_message`, and report the result with the `message_id` so the user can follow up with `edit_message` (own messages, 15 minutes) or `delete_message`.
|
|
32
|
+
|
|
33
|
+
Approval is per message, even after "just send it" for a batch, when the recipient is a group, a number not in the user's contacts, or the content contains money, dates, or commitments. For a batch of plain messages the user already approved as a list, send them one by one and stop at the first error; `RATE_LIMITED` means wait the seconds in `fix`, then continue.
|
|
34
|
+
|
|
35
|
+
## Out of scope
|
|
36
|
+
|
|
37
|
+
`delete_message` with `for_everyone` and `manage_group` remove/leave run only on an explicit ask naming the message or person. Bulk sends to people who did not write first are the user's account at risk of a WhatsApp ban; say that once and let them decide.
|