wazap-mcp 0.9.0 → 0.9.3
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/AGENT.md +17 -0
- package/README.md +40 -10
- package/dist/auth-state.js +4 -1
- package/dist/banner.js +9 -3
- package/dist/bridge.js +64 -0
- package/dist/cli.js +361 -90
- package/dist/config.js +9 -1
- package/dist/connect.js +55 -29
- package/dist/daemon.js +101 -0
- package/dist/doctor.js +17 -2
- package/dist/errors.js +1 -1
- package/dist/ids.js +1 -1
- package/dist/index.js +15 -12
- package/dist/lock.js +24 -1
- package/dist/server.js +31 -18
- package/dist/settings.js +7 -6
- package/dist/setup.js +83 -0
- package/dist/tools.js +1 -1
- package/dist/ui.js +185 -0
- package/dist/whatsapp.js +27 -2
- package/package.json +2 -1
package/AGENT.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# wazap setup, driven by an agent
|
|
2
|
+
|
|
3
|
+
You are setting up WhatsApp access for the person you are working with. Use the pairing-code flow (it is plain text); never the QR.
|
|
4
|
+
|
|
5
|
+
wazap keeps stdout for the MCP protocol and writes every human-readable line to stderr, so read stderr as well: run each command with `2>&1`.
|
|
6
|
+
|
|
7
|
+
1. Run `npx wazap-mcp status --json`. If `linked` is true, skip to step 5.
|
|
8
|
+
2. Ask the person for their WhatsApp number in international format (e.g. +15550100) and whether the agent may send messages (default: no).
|
|
9
|
+
3. Start the link IN THE BACKGROUND and keep reading its output:
|
|
10
|
+
`npx wazap-mcp login --phone <number> --no-writes --yes 2>&1` (or `--writes`).
|
|
11
|
+
Within ~10 s the output contains a line `pairing code: XXXX-XXXX`.
|
|
12
|
+
4. Show the person exactly: "On your phone: WhatsApp → Settings → Linked devices → Link a device → Link with phone number instead → enter XXXX-XXXX". Keep reading the output until `Linked as …` and then `Synced N chats …` appear (up to 3 minutes). If the output says the code expired, run step 3 again.
|
|
13
|
+
5. Register wazap with the client you are running in: `npx wazap-mcp connect <client>` where <client> is claude-code, claude-desktop, cursor, codex, vscode or gemini. Tell the person what the command printed as `Next`.
|
|
14
|
+
6. Verify: `npx wazap-mcp status --json` must show `linked: true` and no failing check. If the client needs a restart, say so; otherwise call the `get_status` tool and then `learn`.
|
|
15
|
+
7. Done when `get_status` returns `connected`. Then offer: "what did I miss on WhatsApp today?"
|
|
16
|
+
|
|
17
|
+
Errors: every wazap error prints `✗ message` and `→ fix`; do what the fix says, do not retry blindly. One process owns the session, so while the `login` of step 3 runs, no server and no other wazap command may touch the same data dir; two servers on one data dir refuse to start. Stop the other one first.
|
package/README.md
CHANGED
|
@@ -19,18 +19,30 @@ WhatsApp multi-device protocol over a WebSocket.
|
|
|
19
19
|
The npm package is `wazap-mcp`; the command it installs is `wazap`.
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
npx wazap-mcp
|
|
23
|
-
npx wazap-mcp connect claude-code # write the MCP entry for your agent
|
|
22
|
+
npx wazap-mcp setup
|
|
24
23
|
```
|
|
25
24
|
|
|
25
|
+
That is the whole install. It links your account, finds the MCP clients
|
|
26
|
+
installed on this machine, writes their config and tells you what to restart.
|
|
27
|
+
|
|
28
|
+
Or have your agent do it. Paste this:
|
|
29
|
+
|
|
30
|
+
*Set up WhatsApp for me: run `npx wazap-mcp setup --agent` and follow what it prints.*
|
|
31
|
+
|
|
26
32
|
Then ask your agent: *"what did I miss on WhatsApp today?"*
|
|
27
33
|
|
|
28
|
-
`
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
Below are the steps `setup` runs for you. Each is still its own command when you
|
|
35
|
+
want to run it by hand.
|
|
36
|
+
|
|
37
|
+
`npx wazap-mcp login` shows a QR code; scan it from **Settings → Linked devices
|
|
38
|
+
→ Link a device**. No camera handy, or linking over SSH? `npx wazap-mcp login --phone +15550100`
|
|
39
|
+
prints an 8-character code you type under *Link with phone number instead*.
|
|
31
40
|
It ends by asking whether the agent may send messages; the answer is no unless
|
|
32
41
|
you say yes, and `npx wazap-mcp config writes on` changes it later.
|
|
33
42
|
|
|
43
|
+
`npx wazap-mcp connect claude-code` writes the MCP entry for one client. The
|
|
44
|
+
table under **Connect a client** has the rest.
|
|
45
|
+
|
|
34
46
|
`npx wazap-mcp` on its own is safe to run: it prints where you stand and what to do
|
|
35
47
|
next, and starts no server. When something is off, `npx wazap-mcp status` is the
|
|
36
48
|
first thing to run — it checks Node, the data directory, the lock, the
|
|
@@ -122,7 +134,7 @@ wazap ships five [Agent Skills](https://agentskills.io) that teach an agent the
|
|
|
122
134
|
|
|
123
135
|
| Skill | What the agent does |
|
|
124
136
|
| --- | --- |
|
|
125
|
-
| `wazap-setup` | Diagnose with `wazap status`, link by pairing code, connect a client with `wazap connect`, repair an expired session |
|
|
137
|
+
| `wazap-setup` | Diagnose with `wazap status`, link by QR or pairing code, connect a client with `wazap connect`, repair an expired session |
|
|
126
138
|
| `whatsapp-inbox` | "What did I miss?" Triage into *needs you / FYI / noise*, ranked, plus forgotten replies. Read-only |
|
|
127
139
|
| `whatsapp-recall` | "Find the invoice Dan sent." Search with query variants, page back in time, download and read the file. Read-only |
|
|
128
140
|
| `whatsapp-groups` | Catch up on a 300-message group: decisions, dates, what is asked of you. Read-only |
|
|
@@ -174,14 +186,32 @@ created `0700` with credentials written `0600`:
|
|
|
174
186
|
history/ per-chat message history, so a restart is not amnesia
|
|
175
187
|
store.json chat-list snapshot
|
|
176
188
|
server.lock pid of the running server
|
|
189
|
+
daemon.json loopback endpoint a second wazap bridges to
|
|
177
190
|
.env optional settings, see .env.example
|
|
178
191
|
```
|
|
179
192
|
|
|
180
193
|
Credential writes go to a temp file and are renamed into place, so killing the
|
|
181
194
|
process mid-write cannot leave you re-linking your phone.
|
|
182
195
|
|
|
183
|
-
|
|
184
|
-
|
|
196
|
+
## Several clients at once
|
|
197
|
+
|
|
198
|
+
Claude Desktop, Claude Code and Cursor each launch their own `wazap`. WhatsApp
|
|
199
|
+
allows one socket per linked device, so they share one session instead of
|
|
200
|
+
fighting over it. The first `wazap` on a data directory owns the session and
|
|
201
|
+
opens an MCP endpoint on `127.0.0.1`; every later one bridges to it over that
|
|
202
|
+
endpoint. There is nothing to configure, and no client can tell the difference.
|
|
203
|
+
The owner publishes `<data-dir>/daemon.json` (`0600`) with its pid, its port
|
|
204
|
+
and the token a bridge authenticates with.
|
|
205
|
+
|
|
206
|
+
A bridge serves whatever the owner exposes, so an owner started `--read-only`
|
|
207
|
+
makes every client read-only, whatever flags that client was launched with.
|
|
208
|
+
|
|
209
|
+
When the owner exits, the bridges exit with it, and the next `wazap` a client
|
|
210
|
+
starts becomes the new owner.
|
|
211
|
+
|
|
212
|
+
`WAZAP_NO_SHARE=1` opts out: a second `wazap` on the same directory exits with
|
|
213
|
+
code 2 naming the pid of the one already running. An explicit `--http` is a
|
|
214
|
+
server of its own rather than a bridge, and is refused the same way.
|
|
185
215
|
|
|
186
216
|
## Read-only mode
|
|
187
217
|
|
|
@@ -218,7 +248,7 @@ Run wazap on a server of your own when the agent is not on your laptop: another
|
|
|
218
248
|
```bash
|
|
219
249
|
npm install -g wazap-mcp
|
|
220
250
|
sudo useradd --system --home /var/lib/wazap --create-home wazap
|
|
221
|
-
sudo -u wazap WAZAP_DATA_DIR=/var/lib/wazap wazap login --phone +
|
|
251
|
+
sudo -u wazap WAZAP_DATA_DIR=/var/lib/wazap wazap login --phone +15550100 # pairing code works over SSH
|
|
222
252
|
sudo -u wazap tee /var/lib/wazap/.env >/dev/null <<END
|
|
223
253
|
WAZAP_READ_TOKEN=$(openssl rand -hex 32)
|
|
224
254
|
WAZAP_WRITE_TOKEN=$(openssl rand -hex 32)
|
|
@@ -235,7 +265,7 @@ The unit binds loopback only. Put TLS in front with the two-line [`deploy/Caddyf
|
|
|
235
265
|
```bash
|
|
236
266
|
git clone https://github.com/razvangirgiz/wazap && cd wazap
|
|
237
267
|
printf 'WAZAP_READ_TOKEN=%s\nWAZAP_WRITE_TOKEN=%s\n' $(openssl rand -hex 32) $(openssl rand -hex 32) > .env
|
|
238
|
-
docker compose run --rm wazap login --phone +
|
|
268
|
+
docker compose run --rm wazap login --phone +15550100 # once; the session lands in the wazap-data volume
|
|
239
269
|
docker compose up -d
|
|
240
270
|
curl -s http://127.0.0.1:8766/healthz
|
|
241
271
|
```
|
package/dist/auth-state.js
CHANGED
|
@@ -106,7 +106,10 @@ export function readLinkedAccount(dir) {
|
|
|
106
106
|
catch {
|
|
107
107
|
throw new WazapError("SESSION_CORRUPT", `Stored credentials in ${dir} are unreadable.`, RESET_FIX);
|
|
108
108
|
}
|
|
109
|
-
|
|
109
|
+
// Baileys itself treats an account with `me` as linked (socket.js: `if (!creds.me)`
|
|
110
|
+
// pairs a device, else logs in). `registered` is only ever set by the
|
|
111
|
+
// pairing-code flow, so a QR-linked session would look unlinked forever.
|
|
112
|
+
if (!creds.me?.id)
|
|
110
113
|
return null;
|
|
111
114
|
const number = creds.me.id.split(":")[0].split("@")[0];
|
|
112
115
|
return { id: `${number}@s.whatsapp.net`, name: creds.me.name || "", number };
|
package/dist/banner.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import { brand, dim } from "./ui.js";
|
|
2
|
+
export const BANNER_ART = `██╗ ██╗ █████╗ ███████╗ █████╗ ██████╗
|
|
2
3
|
██║ ██║██╔══██╗╚══███╔╝██╔══██╗██╔══██╗
|
|
3
4
|
██║ █╗ ██║███████║ ███╔╝ ███████║██████╔╝
|
|
4
5
|
██║███╗██║██╔══██║ ███╔╝ ██╔══██║██╔═══╝
|
|
5
6
|
╚███╔███╔╝██║ ██║███████╗██║ ██║██║
|
|
6
|
-
╚══╝╚══╝ ╚═╝ ╚═╝╚══════╝╚═╝
|
|
7
|
-
WhatsApp for your AI agent
|
|
7
|
+
╚══╝╚══╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝`;
|
|
8
|
+
export const TAGLINE = "WhatsApp for your AI agent.";
|
|
9
|
+
export const BANNER = `${BANNER_ART}\n${TAGLINE}`;
|
|
10
|
+
/** Painted per line, so a wrapped terminal cannot bleed the colour onward. */
|
|
11
|
+
export function banner() {
|
|
12
|
+
return `${BANNER_ART.split("\n").map(brand).join("\n")}\n${dim(TAGLINE)}`;
|
|
13
|
+
}
|
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { CallToolRequestSchema, CallToolResultSchema, ListToolsRequestSchema, ListToolsResultSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { WAZAP_VERSION } from "./config.js";
|
|
7
|
+
import { readDaemon } from "./daemon.js";
|
|
8
|
+
import { log } from "./logger.js";
|
|
9
|
+
const HEARTBEAT_MS = 1_000;
|
|
10
|
+
/**
|
|
11
|
+
* Serve this client from the session another process already owns: an MCP server
|
|
12
|
+
* on our stdio, every tool call forwarded to the daemon's loopback endpoint and
|
|
13
|
+
* its answer returned untouched.
|
|
14
|
+
*
|
|
15
|
+
* `daemonFile` is here because the heartbeat re-reads the sidecar, and DaemonInfo
|
|
16
|
+
* carries no path.
|
|
17
|
+
*/
|
|
18
|
+
export async function runBridge(daemon, daemonFile) {
|
|
19
|
+
let left = false;
|
|
20
|
+
/** Exit 1 so the client restarts us, and the restart becomes the new daemon. */
|
|
21
|
+
const leave = (reason) => {
|
|
22
|
+
if (left)
|
|
23
|
+
return;
|
|
24
|
+
left = true;
|
|
25
|
+
log(`${reason}, exiting so the next start can own the session`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
};
|
|
28
|
+
const client = new Client({ name: "wazap-bridge", version: WAZAP_VERSION });
|
|
29
|
+
await client.connect(new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${daemon.port}/mcp`), {
|
|
30
|
+
requestInit: { headers: { Authorization: `Bearer ${daemon.token}` } },
|
|
31
|
+
}));
|
|
32
|
+
const caps = client.getServerCapabilities() ?? {};
|
|
33
|
+
const server = new Server(client.getServerVersion() ?? { name: "wazap", version: daemon.version }, {
|
|
34
|
+
// Only what we forward: the daemon has no resources or prompts, and we have
|
|
35
|
+
// no handler for them.
|
|
36
|
+
capabilities: { tools: caps.tools ?? {} },
|
|
37
|
+
instructions: client.getInstructions(),
|
|
38
|
+
});
|
|
39
|
+
server.setRequestHandler(ListToolsRequestSchema, (req) => client.request({ method: "tools/list", params: req.params }, ListToolsResultSchema));
|
|
40
|
+
server.setRequestHandler(CallToolRequestSchema, (req) => client.request({ method: "tools/call", params: req.params }, CallToolResultSchema));
|
|
41
|
+
client.onclose = () => leave(`the session holder (pid ${daemon.pid}) closed the connection`);
|
|
42
|
+
client.onerror = () => leave(`lost the connection to the session holder (pid ${daemon.pid})`);
|
|
43
|
+
// A dead daemon does not close the client: the transport retries its stream and
|
|
44
|
+
// reports nothing, measured. So the liveness of the pid is ours to watch.
|
|
45
|
+
const heartbeat = setInterval(() => {
|
|
46
|
+
if (readDaemon(daemonFile)?.pid !== daemon.pid) {
|
|
47
|
+
leave(`the session holder (pid ${daemon.pid}) gave up the session`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
process.kill(daemon.pid, 0);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
leave(`the session holder (pid ${daemon.pid}) is gone`);
|
|
55
|
+
}
|
|
56
|
+
}, HEARTBEAT_MS);
|
|
57
|
+
heartbeat.unref();
|
|
58
|
+
await server.connect(new StdioServerTransport());
|
|
59
|
+
log(`sharing the WhatsApp session held by pid ${daemon.pid}`);
|
|
60
|
+
// Our own client leaving is not a failure. The upstream stream holds the event
|
|
61
|
+
// loop open, so without this the bridge outlives the client it was started for.
|
|
62
|
+
process.stdin.on("end", () => process.exit(0));
|
|
63
|
+
process.stdin.on("close", () => process.exit(0));
|
|
64
|
+
}
|