Skip to main content
Glama
jimingyuan7

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