wechat-mcp
by jimingyuan7
README.md
# wechat-mcp
A **Model Context Protocol (MCP) server** for sending and receiving WeChat
(Weixin) messages, built on Tencent's iLink bot protocol.
This is a port of the messaging core of
[`Tencent/openclaw-weixin`](https://github.com/Tencent/openclaw-weixin) (an
OpenClaw channel plugin) into a standalone MCP server usable from Claude Code,
Claude Desktop, or any MCP client. The WeChat protocol logic — QR login, the
`getUpdates` long-poll receive loop, `sendMessage`, and the AES-128-ECB CDN
media pipeline — is preserved; the OpenClaw runtime/SDK coupling has been
removed and replaced with a thin MCP tool layer.
## What it does
| MCP tool | Purpose |
|---|---|
| `wechat_login` | QR-code login. Prints a QR to the terminal (STDERR); scan with WeChat mobile and confirm. Persists the bot token. |
| `wechat_list_accounts` | List logged-in WeChat bot accounts. |
| `wechat_logout` | Remove an account's stored credentials, sync cursor, and context tokens. |
| `wechat_send` | Send text and/or a media attachment (image / video / file) to a user. Accepts a local path or a remote http(s) URL. Text is markdown-filtered by default (WeChat-unsupported syntax stripped); pass `filterMarkdown: false` for raw text. |
| `wechat_receive` | Poll for new inbound messages (one long-poll cycle). Tracks a per-account sync cursor so repeated calls don't return duplicates. Inbound media is downloaded + decrypted to local temp files. |
| `wechat_listen` | Continuously poll until a message arrives, an error occurs, or the window elapses (default 2 min). Re-polls back-to-back — the reliable way to wait for a message, since a single `wechat_receive` cycle often returns empty early. |
| `wechat_typing` | Show (or cancel) the "typing…" indicator for a user. The typing ticket is resolved automatically. |
## Requirements
- Node.js >= 20
- A WeChat (Weixin) mobile app to scan the login QR code
## Install
Clone and install. The `prepare` hook compiles TypeScript to `dist/`
automatically on `npm install`, so there is no separate build step.
```bash
git clone https://github.com/jimingyuan7/wechat-mcp.git wechat-mcp
cd wechat-mcp
npm install
```
To rebuild after editing source: `npm run build`.
Optional: voice-message transcoding (SILK → WAV) requires the optional
`silk-wasm` package. Without it, inbound voice is saved as raw `.silk`.
```bash
npm install silk-wasm
```
## First-time login
Run the interactive login in a real terminal (the QR renders to STDERR):
```bash
npm run login
```
Scan the QR code with the WeChat mobile app and confirm. Credentials are saved
under `~/.wechat-mcp/openclaw-weixin/accounts/`.
You can also trigger login through the `wechat_login` MCP tool, but a real
terminal is friendlier for scanning the QR.
## Register with an MCP client
### Claude Code
**Prerequisites:** the project is built (`npm install` already ran the `prepare`
build, so `dist/mcp/server.js` exists) and you have logged in once
(`npm run login`).
**1. Add the server.** Run this from anywhere — use the absolute path to the
built entry point:
```bash
claude mcp add wechat -- node /absolute/path/to/wechat-mcp/dist/mcp/server.js
```
Tip: if you're inside the project dir, `$(pwd)` fills the path in for you:
```bash
claude mcp add wechat -- node "$(pwd)/dist/mcp/server.js"
```
By default the server is added at **local** scope (only this project on this
machine). To make it available across all your projects, use user scope:
```bash
claude mcp add -s user wechat -- node /absolute/path/to/wechat-mcp/dist/mcp/server.js
```
To override a config env var (e.g. a custom state dir), pass `-e`:
```bash
claude mcp add wechat \
-e WECHAT_MCP_STATE_DIR=/data/wechat \
-- node /absolute/path/to/wechat-mcp/dist/mcp/server.js
```
**2. Verify it's connected:**
```bash
claude mcp list # should show: wechat ✓ connected
claude mcp get wechat # shows the full command + health
```
**3. Use it.** Start `claude`, and the 7 `wechat_*` tools are available. Just
ask in natural language, e.g.:
> *"Use wechat_listen to wait for a WeChat message, then reply with a friendly greeting."*
**Remove** when you no longer need it:
```bash
claude mcp remove wechat
```
> Note: first-time WeChat login (`npm run login`) needs a real terminal to scan
> the QR code, so do that **before** relying on the tools inside Claude Code.
> Credentials persist under `~/.wechat-mcp/`, so you only log in once.
### Claude Desktop (`claude_desktop_config.json`)
```jsonc
{
"mcpServers": {
"wechat": {
"command": "node",
"args": ["/absolute/path/to/wechat-mcp/dist/mcp/server.js"]
}
}
}
```
## Usage notes
- **Recipient ids** look like `xxxxxxxx@im.wechat`. You normally obtain one
from an inbound message (`wechat_receive` → message `From`).
- **Context tokens**: the WeChat backend issues a per-conversation
`context_token` on each inbound message that must be echoed on outbound sends.
The server caches these automatically (in memory + on disk) as messages
arrive, so `wechat_send` to a user who has recently messaged the bot "just
works". Sending to a user with no cached token may be rejected by the
backend.
- **Receiving** is poll-based: call `wechat_receive` repeatedly (e.g. in a
loop). Each call holds the connection open up to `timeoutMs` (default 35s)
waiting for new messages, then returns. The sync cursor is persisted, so you
never see the same message twice across calls or restarts.
- **Media**: outbound media is auto-classified by file extension
(`video/*`, `image/*`, else generic file). Inbound media is downloaded,
AES-128-ECB decrypted, and written to `~/.wechat-mcp/tmp/media/inbound/`;
the local path comes back in the message's `MediaPath`.
## Usage examples
> In a chat with an MCP client (e.g. Claude Code / Claude Desktop) you just ask
> in natural language — "reply to the last WeChat message", "send this photo to
> the user", etc. The tool-call payloads below show what the client sends under
> the hood, and are also handy for direct/manual testing.
The recommended flow is **receive first, then reply**: an inbound message caches
the `context_token` that outbound sends require.
### 1. See who's logged in
```json
{ "name": "wechat_list_accounts", "arguments": {} }
```
```jsonc
// → result
{ "count": 1, "accounts": [
{ "accountId": "bfa52ff0d915-im-bot",
"userId": "o9cq...@im.wechat", "configured": true }
] }
```
### 2. Wait for an incoming message (recommended over wechat_receive)
`wechat_listen` re-polls until a message arrives or the window elapses:
```json
{ "name": "wechat_listen", "arguments": { "windowMs": 120000 } }
```
```jsonc
// → result (returns as soon as a message arrives)
{ "messages": [
{ "From": "o9cq...@im.wechat", "Body": "Hello",
"MediaPath": null, "context_token": "AARz..." }
], "pollCycles": 3, "timedOut": false }
```
Copy `From` — that's the `to` you reply to. The `context_token` is now cached,
so the next send will actually deliver.
### 3. Reply with text
```json
{ "name": "wechat_send", "arguments": {
"to": "o9cq...@im.wechat", "text": "Hi! Got it 👍" } }
```
```jsonc
// → result
{ "messageId": "wechat-mcp:...", "hadContextToken": true, "markdownFiltered": false }
```
> `hadContextToken: true` means it will be delivered. If it's `false`, the
> recipient hasn't messaged the bot yet — have them send one message first.
### 4. "Typing…" indicator before a slow reply
```json
{ "name": "wechat_typing", "arguments": { "to": "o9cq...@im.wechat" } }
```
…do your slow work (call an LLM, fetch data), then `wechat_send` the result.
Cancel the indicator early with `{ "to": "...", "status": "cancel" }`.
### 5. Send an image or file
Local path (absolute recommended) or a remote URL — type is auto-detected:
```json
{ "name": "wechat_send", "arguments": {
"to": "o9cq...@im.wechat", "text": "Here's the photo", "media": "/tmp/photo.png" } }
```
```json
{ "name": "wechat_send", "arguments": {
"to": "o9cq...@im.wechat", "media": "https://example.com/cat.jpg" } }
```
### 6. Markdown handling
Outbound text is markdown-filtered by default — WeChat-unsupported syntax
(H5/H6 headings, CJK italics `*…*`, inline images) is stripped so users see
clean text instead of stray symbols. Pass `filterMarkdown: false` to send raw:
```json
{ "name": "wechat_send", "arguments": {
"to": "o9cq...@im.wechat", "text": "raw **markdown** stays", "filterMarkdown": false } }
```
> Note: WeChat chat bubbles do **not** render rich text at all — filtering only
> removes noisy markers; it cannot make text bold/italic on the WeChat side.
### Quick CLI smoke test (no MCP client)
You can drive the server over stdio directly:
```bash
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"cli","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"wechat_list_accounts","arguments":{}}}' \
| node dist/mcp/server.js
```
## Configuration (environment variables)
| Variable | Default | Description |
|---|---|---|
| `WECHAT_MCP_STATE_DIR` | `~/.wechat-mcp` | Where credentials, sync cursors, and context tokens are stored. |
| `WECHAT_MCP_TMP_DIR` | `<state>/tmp` | Temp dir for downloaded / decrypted media. |
| `WECHAT_MCP_LOG_LEVEL` | `INFO` | `TRACE` `DEBUG` `INFO` `WARN` `ERROR`. Logs go to STDERR. |
| `WECHAT_MCP_BOT_AGENT` | `WeChatMCP` | UA-style self-identifier sent on every request (for backend log attribution). |
| `WECHAT_MCP_CDN_BASE_URL` | Tencent C2C CDN | Override the media CDN base. |
| `WECHAT_MCP_ROUTE_TAG` | — | Optional `SKRouteTag` header. |
## Architecture
```
src/
api/ iLink HTTP+JSON protocol (getUpdates, sendMessage, getUploadUrl, …) + types
auth/ QR login flow + per-account credential store
cdn/ AES-128-ECB encrypt/decrypt + CDN upload/download
media/ MIME mapping, media download/decrypt, optional SILK→WAV transcode
messaging/ send (text/image/video/file), inbound normalization + context tokens,
receive (single cycle + receiveUntil listen loop), outbound (high-level
send w/ markdown filter), typing (indicator), markdown-filter
storage/ state-dir resolution + sync-buf (getUpdates cursor) persistence
util/ logger (STDERR-only), redaction, id/account-id helpers
mcp/ MCP stdio server exposing the 5 tools
```
The STDOUT stream is reserved exclusively for the MCP JSON-RPC protocol; all
human-facing output (logs, QR codes, prompts) goes to STDERR.
## Credits
Protocol implementation ported from
[`Tencent/openclaw-weixin`](https://github.com/Tencent/openclaw-weixin) (MIT).
TDQS
A4.3/5.0
Scored across 7 tools
Disambiguation5/5
Each tool has a clearly distinct purpose: listing accounts, listening for messages, logging in/out, receiving, sending, and typing. There is no functional overlap.
Naming Consistency5/5
All tools follow a consistent 'wechat_verb_noun' pattern (e.g., wechat_list_accounts, wechat_send), making the naming predictable and easy to understand.
Tool Count5/5
With 7 tools, the set is well-scoped for a WeChat integration, covering core operations without being excessive or insufficient.
Completeness4/5
The tool set covers essential operations (login, logout, send, receive, typing) but lacks tools for managing contacts, groups, or deleting messages, which are common use cases.
Maintenance
ActivityInactive
ResponsivenessNo issues