weixin-mcp 1.4.2 → 1.5.0

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.en.md CHANGED
@@ -1,28 +1,54 @@
1
1
  # weixin-mcp
2
2
 
3
- Standalone MCP server for WeChat — expose WeChat messaging as MCP tools for Claude Desktop and other MCP clients.
3
+ MCP server for WeChat messaging — expose WeChat capabilities as MCP tools for Claude Desktop, Cursor, and other MCP clients.
4
4
 
5
- Reuses the token from [OpenClaw weixin plugin](https://www.npmjs.com/package/@tencent-weixin/openclaw-weixin) if already installed, or supports independent QR login.
5
+ [中文](./README.md)
6
6
 
7
7
  ## Quick Start
8
8
 
9
- ### Step 1 — Login (first time only)
10
-
11
9
  ```bash
12
- npx weixin-login
13
- ```
10
+ # 1. Login (scan QR code)
11
+ npx weixin-mcp login
12
+
13
+ # 2. Check status
14
+ npx weixin-mcp status
14
15
 
15
- A QR code will appear in your terminal. Scan it with WeChat and confirm. Token is saved locally.
16
+ # 3. Start MCP server (stdio mode for Claude Desktop)
17
+ npx weixin-mcp
18
+ ```
16
19
 
17
- ### Step 2 — Start the MCP server
20
+ ## CLI Commands
21
+
22
+ | Command | Description |
23
+ |---------|-------------|
24
+ | `npx weixin-mcp login` | QR code login |
25
+ | `npx weixin-mcp status` | Show account and daemon status |
26
+ | `npx weixin-mcp` | Start stdio MCP server (Claude Desktop) |
27
+ | `npx weixin-mcp start [--port n]` | Start HTTP daemon (background, default 3001) |
28
+ | `npx weixin-mcp stop` | Stop daemon |
29
+ | `npx weixin-mcp restart` | Restart daemon |
30
+ | `npx weixin-mcp logs [-f]` | View daemon logs (-f for follow) |
31
+ | `npx weixin-mcp send <userId> <text>` | Send message (supports short ID prefix) |
32
+ | `npx weixin-mcp poll [--watch] [--reset]` | Poll messages (--watch for continuous) |
33
+ | `npx weixin-mcp contacts` | List contacts (users who messaged the bot) |
34
+ | `npx weixin-mcp accounts [list]` | List all accounts |
35
+ | `npx weixin-mcp accounts remove <id>` | Remove an account |
36
+ | `npx weixin-mcp accounts clean` | Remove duplicates (keep newest per userId) |
37
+ | `npx weixin-mcp update` | Check and install latest version |
38
+ | `npx weixin-mcp --version` | Print version |
39
+
40
+ ### Short ID Matching
41
+
42
+ When sending messages, you can use a prefix of the user ID if it uniquely matches a contact:
18
43
 
19
44
  ```bash
20
- npx weixin-mcp
45
+ npx weixin-mcp send o9cq8 "hello"
46
+ # Resolved "o9cq8" → o9cq80x8ou646cs3Tt5EQgfsZRtI@im.wechat
21
47
  ```
22
48
 
23
49
  ## Claude Desktop Integration
24
50
 
25
- Add to your `claude_desktop_config.json`:
51
+ Add to `claude_desktop_config.json`:
26
52
 
27
53
  ```json
28
54
  {
@@ -35,34 +61,46 @@ Add to your `claude_desktop_config.json`:
35
61
  }
36
62
  ```
37
63
 
38
- Restart Claude Desktop. You can now ask Claude to send WeChat messages or poll for new ones.
64
+ ## HTTP Daemon Mode
39
65
 
40
- ## Tools
66
+ Start an HTTP daemon for multi-client connections:
41
67
 
42
- | Tool | Description | Parameters |
43
- |------|-------------|------------|
44
- | `weixin_send` | Send a text message | `to` (user ID), `text`, `context_token` (optional — link reply to a conversation) |
45
- | `weixin_poll` | Poll for new messages (cursor-based, no duplicates) | `reset_cursor` (optional boolean) |
46
- | `weixin_get_config` | Get user config (typing ticket, etc.) | `user_id`, `context_token` (optional) |
68
+ ```bash
69
+ npx weixin-mcp start --port 3001
70
+ ```
47
71
 
48
- ## Already using OpenClaw weixin plugin?
72
+ - MCP endpoint: `http://localhost:3001/mcp` (StreamableHTTP)
73
+ - Health check: `http://localhost:3001/health`
49
74
 
50
- If you've already logged in via OpenClaw, no login needed — `npx weixin-mcp` will pick up the existing token automatically.
75
+ ## MCP Tools
51
76
 
52
- ## Environment Variables
77
+ | Tool | Description | Parameters |
78
+ |------|-------------|------------|
79
+ | `weixin_send` | Send text message | `to`, `text`, `context_token` (optional) |
80
+ | `weixin_poll` | Poll new messages | `reset_cursor` (optional) |
81
+ | `weixin_contacts` | List contacts | none |
82
+ | `weixin_get_config` | Get user config | `user_id`, `context_token` (optional) |
53
83
 
54
- | Variable | Default | Description |
55
- |----------|---------|-------------|
56
- | `OPENCLAW_STATE_DIR` | `~/.openclaw` | State directory; accounts are read from `$OPENCLAW_STATE_DIR/openclaw-weixin/accounts/` |
57
- | `WEIXIN_ACCOUNT_ID` | first account found | Specify which account to use (filename without `.json`) |
84
+ ## Data Storage
58
85
 
59
- ## Re-login
86
+ Priority:
87
+ 1. `WEIXIN_MCP_DIR` environment variable
88
+ 2. `~/.openclaw/openclaw-weixin/` (if OpenClaw installed)
89
+ 3. `~/.weixin-mcp/` (default)
60
90
 
61
- If your token expires:
91
+ Files:
92
+ - `accounts/<accountId>.json` — account token
93
+ - `accounts/<accountId>.cursor.json` — message cursor
94
+ - `contacts.json` — contact book
95
+ - `daemon.json` — daemon PID (HTTP mode only)
96
+ - `daemon.log` — daemon logs
62
97
 
63
- ```bash
64
- npx weixin-login
65
- ```
98
+ ## Environment Variables
99
+
100
+ | Variable | Description |
101
+ |----------|-------------|
102
+ | `WEIXIN_MCP_DIR` | Custom data directory |
103
+ | `WEIXIN_ACCOUNT_ID` | Specify which account to use |
66
104
 
67
105
  ## License
68
106
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # weixin-mcp
2
2
 
3
- 基于 MCP 协议的微信消息服务端——将微信能力暴露为 MCP 工具,供 Claude Desktop 及其他 MCP 客户端使用。
3
+ 基于 MCP 协议的微信消息服务端——将微信能力暴露为 MCP 工具,供 Claude Desktop、Cursor 及其他 MCP 客户端使用。
4
4
 
5
5
  支持复用 [OpenClaw weixin 插件](https://www.npmjs.com/package/@tencent-weixin/openclaw-weixin) 的已有登录态,或独立扫码登录。
6
6
 
@@ -8,18 +8,44 @@
8
8
 
9
9
  ## 快速开始
10
10
 
11
- ### 第一步 — 登录(首次使用)
12
-
13
11
  ```bash
14
- npx weixin-login
15
- ```
12
+ # 1. 登录(首次使用,扫码)
13
+ npx weixin-mcp login
14
+
15
+ # 2. 查看状态
16
+ npx weixin-mcp status
16
17
 
17
- 终端会显示二维码,微信扫码确认后 token 自动保存到本地。
18
+ # 3. 启动 MCP server(Claude Desktop 模式)
19
+ npx weixin-mcp
20
+ ```
18
21
 
19
- ### 第二步 — 启动 MCP 服务
22
+ ## CLI 命令一览
23
+
24
+ | 命令 | 说明 |
25
+ |------|------|
26
+ | `npx weixin-mcp login` | 扫码登录微信 |
27
+ | `npx weixin-mcp status` | 查看账号和 daemon 状态 |
28
+ | `npx weixin-mcp` | 启动 stdio MCP server(Claude Desktop) |
29
+ | `npx weixin-mcp start [--port n]` | 启动 HTTP daemon(后台,默认端口 3001) |
30
+ | `npx weixin-mcp stop` | 停止 daemon |
31
+ | `npx weixin-mcp restart` | 重启 daemon |
32
+ | `npx weixin-mcp logs [-f]` | 查看 daemon 日志(-f 实时跟踪) |
33
+ | `npx weixin-mcp send <userId> <text>` | 发送消息(支持短 ID 前缀匹配) |
34
+ | `npx weixin-mcp poll [--watch] [--reset]` | 拉取消息(--watch 持续监听) |
35
+ | `npx weixin-mcp contacts` | 查看联系人(给 bot 发过消息的用户) |
36
+ | `npx weixin-mcp accounts [list]` | 列出所有账号 |
37
+ | `npx weixin-mcp accounts remove <id>` | 删除账号 |
38
+ | `npx weixin-mcp accounts clean` | 清理重复账号(同 userId 保留最新) |
39
+ | `npx weixin-mcp update` | 检查并更新到最新版 |
40
+ | `npx weixin-mcp --version` | 查看版本 |
41
+
42
+ ### 短 ID 匹配
43
+
44
+ 发送消息时可以用用户 ID 的前缀,只要在联系人中唯一匹配即可:
20
45
 
21
46
  ```bash
22
- npx weixin-mcp
47
+ npx weixin-mcp send o9cq8 "hello"
48
+ # Resolved "o9cq8" → o9cq80x8ou646cs3Tt5EQgfsZRtI@im.wechat
23
49
  ```
24
50
 
25
51
  ## Claude Desktop 集成
@@ -37,32 +63,48 @@ npx weixin-mcp
37
63
  }
38
64
  ```
39
65
 
40
- 重启 Claude Desktop 后,即可让 Claude 直接发送微信消息或拉取新消息。
66
+ 重启 Claude Desktop 后即可使用。
67
+
68
+ ## HTTP Daemon 模式
41
69
 
42
- ## 工具列表
70
+ 除了 stdio 模式,也可以启动 HTTP daemon 供多客户端连接:
71
+
72
+ ```bash
73
+ npx weixin-mcp start --port 3001
74
+ ```
75
+
76
+ MCP 端点:`http://localhost:3001/mcp`(StreamableHTTP)
77
+ 健康检查:`http://localhost:3001/health`
78
+
79
+ ## MCP 工具列表
43
80
 
44
81
  | 工具名 | 说明 | 参数 |
45
82
  |--------|------|------|
46
- | `weixin_send` | 发送文本消息 | `to`(用户 ID)、`text`、`context_token`(可选,关联会话) |
47
- | `weixin_poll` | 拉取新消息(基于游标,不重复) | `reset_cursor`(可选布尔值) |
48
- | `weixin_get_config` | 获取用户配置(typing ticket 等) | `user_id`、`context_token`(可选) |
49
-
50
- ## 已有 OpenClaw weixin 插件?
83
+ | `weixin_send` | 发送文本消息 | `to`、`text`、`context_token`(可选) |
84
+ | `weixin_poll` | 拉取新消息 | `reset_cursor`(可选) |
85
+ | `weixin_contacts` | 列出联系人 | |
86
+ | `weixin_get_config` | 获取用户配置 | `user_id`、`context_token`(可选) |
51
87
 
52
- 如果已通过 OpenClaw 完成微信登录,无需重新登录——`npx weixin-mcp` 会自动复用已有 token。
88
+ ## 数据存储路径
53
89
 
54
- ## 环境变量
90
+ 优先级:
91
+ 1. `WEIXIN_MCP_DIR` 环境变量
92
+ 2. `~/.openclaw/openclaw-weixin/`(已装 OpenClaw)
93
+ 3. `~/.weixin-mcp/`(默认)
55
94
 
56
- | 变量名 | 默认值 | 说明 |
57
- |--------|--------|------|
58
- | `OPENCLAW_STATE_DIR` | `~/.openclaw` | 状态目录,账号文件读取路径为 `$OPENCLAW_STATE_DIR/openclaw-weixin/accounts/` |
59
- | `WEIXIN_ACCOUNT_ID` | 目录中第一个账号 | 指定使用哪个账号(对应文件名去掉 `.json`) |
95
+ 文件:
96
+ - `accounts/<accountId>.json` — 账号 token
97
+ - `accounts/<accountId>.cursor.json` 消息游标
98
+ - `contacts.json` 联系人
99
+ - `daemon.json` — daemon PID(仅 HTTP 模式)
100
+ - `daemon.log` — daemon 日志
60
101
 
61
- ## Token 过期重新登录
102
+ ## 环境变量
62
103
 
63
- ```bash
64
- npx weixin-login
65
- ```
104
+ | 变量名 | 说明 |
105
+ |--------|------|
106
+ | `WEIXIN_MCP_DIR` | 自定义数据目录 |
107
+ | `WEIXIN_ACCOUNT_ID` | 指定使用哪个账号 |
66
108
 
67
109
  ## License
68
110
 
package/dist/index.js CHANGED
@@ -11,6 +11,16 @@ import path from "node:path";
11
11
  import { DEFAULT_BASE_URL, getUpdates, getConfig, sendTextMessage, loadCursor, saveCursor, WeixinAuthError, WeixinNetworkError, } from "./api.js";
12
12
  import { ACCOUNTS_DIR } from "./paths.js";
13
13
  import { updateContactsFromMsgs, loadContacts } from "./contacts.js";
14
+ /** Resolve short userId prefix to full ID from contacts. */
15
+ function resolveUserId(input, contacts) {
16
+ if (!input || input.includes("@"))
17
+ return input;
18
+ const ids = Object.keys(contacts);
19
+ const matches = ids.filter((id) => id.startsWith(input) || id.includes(input));
20
+ if (matches.length === 1)
21
+ return matches[0];
22
+ return input; // ambiguous or not found — use as-is
23
+ }
14
24
  // ── Auth / config ──────────────────────────────────────────────────────────
15
25
  const WEIXIN_DIR = ACCOUNTS_DIR;
16
26
  function loadAccount() {
@@ -52,7 +62,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
52
62
  inputSchema: {
53
63
  type: "object",
54
64
  properties: {
55
- to: { type: "string", description: "Recipient user ID / OpenId" },
65
+ to: { type: "string", description: "Recipient user ID (full or short prefix if unique in contacts)" },
56
66
  text: { type: "string", description: "Message text to send" },
57
67
  context_token: {
58
68
  type: "string",
@@ -103,8 +113,9 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
103
113
  if (name === "weixin_send") {
104
114
  const { to, text, context_token } = (args ?? {});
105
115
  const validatedTo = assertNonEmptyString(to, "to");
116
+ const resolvedTo = resolveUserId(validatedTo, loadContacts());
106
117
  const validatedText = assertNonEmptyString(text, "text");
107
- result = await sendTextMessage(validatedTo, validatedText, token, baseUrl, context_token);
118
+ result = await sendTextMessage(resolvedTo, validatedText, token, baseUrl, context_token);
108
119
  }
109
120
  else if (name === "weixin_poll") {
110
121
  const { reset_cursor } = (args ?? {});
package/dist/messaging.js CHANGED
@@ -7,7 +7,25 @@ import fs from "node:fs";
7
7
  import path from "node:path";
8
8
  import { ACCOUNTS_DIR } from "./paths.js";
9
9
  import { DEFAULT_BASE_URL, sendTextMessage, getUpdates, loadCursor, saveCursor, } from "./api.js";
10
- import { updateContactsFromMsgs } from "./contacts.js";
10
+ import { updateContactsFromMsgs, loadContacts } from "./contacts.js";
11
+ /** Resolve a short/partial userId to a full one from contacts. */
12
+ function resolveUserId(input) {
13
+ if (!input)
14
+ return input;
15
+ // Already looks like a full id? return as-is
16
+ if (input.includes("@"))
17
+ return input;
18
+ const contacts = Object.keys(loadContacts());
19
+ const matches = contacts.filter((id) => id.startsWith(input) || id.includes(input));
20
+ if (matches.length === 1)
21
+ return matches[0];
22
+ if (matches.length > 1) {
23
+ console.error(`Ambiguous user "${input}", matches:\n${matches.map((m) => ` ${m}`).join("\n")}`);
24
+ process.exit(1);
25
+ }
26
+ // Not found in contacts — treat as literal
27
+ return input;
28
+ }
11
29
  function loadAccount() {
12
30
  const files = fs.readdirSync(ACCOUNTS_DIR).filter((f) => f.endsWith(".json") && !f.endsWith(".sync.json") && !f.endsWith(".cursor.json"));
13
31
  if (files.length === 0)
@@ -41,9 +59,12 @@ export async function cliSend(args) {
41
59
  process.exit(1);
42
60
  }
43
61
  const text = textParts.join(" ");
62
+ const resolvedTo = resolveUserId(to);
63
+ if (resolvedTo !== to)
64
+ console.log(`Resolved "${to}" → ${resolvedTo}`);
44
65
  const { token, baseUrl = DEFAULT_BASE_URL } = loadAccount();
45
- process.stdout.write(`Sending to ${to}... `);
46
- const result = await sendTextMessage(to, text, token, baseUrl);
66
+ process.stdout.write(`Sending to ${resolvedTo}... `);
67
+ const result = await sendTextMessage(resolvedTo, text, token, baseUrl);
47
68
  const ret = result?.ret ?? result?.errcode;
48
69
  if (ret === 0 || ret === undefined) {
49
70
  console.log("✅ Sent");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weixin-mcp",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "MCP server for WeChat (Weixin) — send messages via OpenClaw weixin plugin auth",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/index.ts CHANGED
@@ -23,7 +23,16 @@ import {
23
23
  WeixinNetworkError,
24
24
  } from "./api.js";
25
25
  import { ACCOUNTS_DIR } from "./paths.js";
26
- import { updateContactsFromMsgs, loadContacts } from "./contacts.js";
26
+ import { updateContactsFromMsgs, loadContacts, type ContactBook } from "./contacts.js";
27
+
28
+ /** Resolve short userId prefix to full ID from contacts. */
29
+ function resolveUserId(input: string, contacts: ContactBook): string {
30
+ if (!input || input.includes("@")) return input;
31
+ const ids = Object.keys(contacts);
32
+ const matches = ids.filter((id) => id.startsWith(input) || id.includes(input));
33
+ if (matches.length === 1) return matches[0];
34
+ return input; // ambiguous or not found — use as-is
35
+ }
27
36
 
28
37
  // ── Auth / config ──────────────────────────────────────────────────────────
29
38
 
@@ -85,7 +94,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
85
94
  inputSchema: {
86
95
  type: "object",
87
96
  properties: {
88
- to: { type: "string", description: "Recipient user ID / OpenId" },
97
+ to: { type: "string", description: "Recipient user ID (full or short prefix if unique in contacts)" },
89
98
  text: { type: "string", description: "Message text to send" },
90
99
  context_token: {
91
100
  type: "string",
@@ -149,9 +158,10 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
149
158
  context_token?: string;
150
159
  };
151
160
  const validatedTo = assertNonEmptyString(to, "to");
161
+ const resolvedTo = resolveUserId(validatedTo, loadContacts());
152
162
  const validatedText = assertNonEmptyString(text, "text");
153
163
  result = await sendTextMessage(
154
- validatedTo,
164
+ resolvedTo,
155
165
  validatedText,
156
166
  token!,
157
167
  baseUrl,
package/src/messaging.ts CHANGED
@@ -14,7 +14,23 @@ import {
14
14
  loadCursor,
15
15
  saveCursor,
16
16
  } from "./api.js";
17
- import { updateContactsFromMsgs } from "./contacts.js";
17
+ import { updateContactsFromMsgs, loadContacts } from "./contacts.js";
18
+
19
+ /** Resolve a short/partial userId to a full one from contacts. */
20
+ function resolveUserId(input: string): string {
21
+ if (!input) return input;
22
+ // Already looks like a full id? return as-is
23
+ if (input.includes("@")) return input;
24
+ const contacts = Object.keys(loadContacts());
25
+ const matches = contacts.filter((id) => id.startsWith(input) || id.includes(input));
26
+ if (matches.length === 1) return matches[0];
27
+ if (matches.length > 1) {
28
+ console.error(`Ambiguous user "${input}", matches:\n${matches.map((m) => ` ${m}`).join("\n")}`);
29
+ process.exit(1);
30
+ }
31
+ // Not found in contacts — treat as literal
32
+ return input;
33
+ }
18
34
 
19
35
  interface AccountData { token?: string; baseUrl?: string; userId?: string }
20
36
 
@@ -50,10 +66,12 @@ export async function cliSend(args: string[]) {
50
66
  process.exit(1);
51
67
  }
52
68
  const text = textParts.join(" ");
69
+ const resolvedTo = resolveUserId(to);
70
+ if (resolvedTo !== to) console.log(`Resolved "${to}" → ${resolvedTo}`);
53
71
  const { token, baseUrl = DEFAULT_BASE_URL } = loadAccount();
54
72
 
55
- process.stdout.write(`Sending to ${to}... `);
56
- const result = await sendTextMessage(to, text, token!, baseUrl) as Record<string, unknown>;
73
+ process.stdout.write(`Sending to ${resolvedTo}... `);
74
+ const result = await sendTextMessage(resolvedTo, text, token!, baseUrl) as Record<string, unknown>;
57
75
  const ret = result?.ret ?? result?.errcode;
58
76
  if (ret === 0 || ret === undefined) {
59
77
  console.log("✅ Sent");