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 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 login # link your account with a pairing code
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
- `login` asks for your number in international format, prints an 8-character
29
- code, and you enter it on your phone under **Settings → Linked devices → Link a
30
- device → Link with phone number instead**. Prefer a QR code? `npx wazap-mcp login --qr`.
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
- One server per data directory: a second `wazap serve` on the same directory
184
- exits with code 2 and tells you the pid of the one already running.
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 +40722123456 # pairing code works over SSH
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 +40722123456 # once; the session lands in the wazap-data volume
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
  ```
@@ -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
- if (!creds.registered || !creds.me?.id)
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
- export const BANNER = `██╗ ██╗ █████╗ ███████╗ █████╗ ██████╗
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
+ }