@paean-ai/wechat-mcp 0.1.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.
@@ -0,0 +1,195 @@
1
+ interface AccountData {
2
+ token: string;
3
+ baseUrl: string;
4
+ accountId: string;
5
+ userId?: string;
6
+ savedAt: string;
7
+ }
8
+ interface QRCodeResponse {
9
+ qrcode: string;
10
+ qrcode_img_content: string;
11
+ }
12
+ interface QRStatusResponse {
13
+ status: "wait" | "scaned" | "confirmed" | "expired";
14
+ bot_token?: string;
15
+ ilink_bot_id?: string;
16
+ baseurl?: string;
17
+ ilink_user_id?: string;
18
+ }
19
+ interface TextItem {
20
+ text?: string;
21
+ }
22
+ interface RefMessage {
23
+ message_item?: MessageItem;
24
+ title?: string;
25
+ }
26
+ interface MessageItem {
27
+ type?: number;
28
+ text_item?: TextItem;
29
+ voice_item?: {
30
+ text?: string;
31
+ };
32
+ ref_msg?: RefMessage;
33
+ }
34
+ interface WeixinMessage {
35
+ from_user_id?: string;
36
+ to_user_id?: string;
37
+ client_id?: string;
38
+ session_id?: string;
39
+ message_type?: number;
40
+ message_state?: number;
41
+ item_list?: MessageItem[];
42
+ context_token?: string;
43
+ create_time_ms?: number;
44
+ }
45
+ interface GetUpdatesResp {
46
+ ret?: number;
47
+ errcode?: number;
48
+ errmsg?: string;
49
+ msgs?: WeixinMessage[];
50
+ get_updates_buf?: string;
51
+ longpolling_timeout_ms?: number;
52
+ }
53
+ interface AgentRegistration {
54
+ agentId: string;
55
+ name: string;
56
+ registeredAt: string;
57
+ }
58
+ interface RoutedMessage {
59
+ id: string;
60
+ senderId: string;
61
+ senderName: string;
62
+ text: string;
63
+ rawText: string;
64
+ contextToken: string;
65
+ targetAgent: string | null;
66
+ timestamp: number;
67
+ }
68
+ interface WechatContact {
69
+ userId: string;
70
+ contextToken: string;
71
+ lastSeen: string;
72
+ displayName?: string;
73
+ }
74
+ interface DaemonInfo {
75
+ pid: number;
76
+ port: number;
77
+ startedAt: string;
78
+ }
79
+ interface DaemonStatus {
80
+ running: boolean;
81
+ wechatConnected: boolean;
82
+ accountId?: string;
83
+ agents: AgentRegistration[];
84
+ uptime: number;
85
+ }
86
+ interface SendMessageRequest {
87
+ to: string;
88
+ text: string;
89
+ agentId: string;
90
+ }
91
+ interface SendMessageResponse {
92
+ success: boolean;
93
+ error?: string;
94
+ }
95
+ interface PollMessagesResponse {
96
+ messages: RoutedMessage[];
97
+ }
98
+
99
+ /**
100
+ * Unified WeChat iLink API Client
101
+ *
102
+ * Handles all communication with the WeChat iLink bot API:
103
+ * QR login, long-polling for messages, and sending replies.
104
+ */
105
+
106
+ declare function fetchQRCode(baseUrl: string): Promise<QRCodeResponse>;
107
+ declare function pollQRStatus(baseUrl: string, qrcode: string): Promise<QRStatusResponse>;
108
+ declare function getUpdates(baseUrl: string, token: string, getUpdatesBuf: string): Promise<GetUpdatesResp>;
109
+ declare function sendTextMessage(baseUrl: string, token: string, to: string, text: string, contextToken: string): Promise<string>;
110
+ declare function extractTextFromMessage(msg: WeixinMessage): string;
111
+
112
+ /**
113
+ * Credential & Contact Storage
114
+ *
115
+ * All state is stored under ~/.wechat-mcp/ with restrictive permissions.
116
+ */
117
+
118
+ declare function loadCredentials(): AccountData | null;
119
+ declare function saveCredentials(data: AccountData): void;
120
+ declare function removeCredentials(): void;
121
+ declare function loadContacts(): WechatContact[];
122
+ declare function saveContact(userId: string, contextToken: string): void;
123
+ declare function getContactToken(userId: string): string | null;
124
+
125
+ /**
126
+ * Daemon Lifecycle Management
127
+ *
128
+ * Handles auto-starting, health checking, and stopping the daemon process.
129
+ * MCP server processes call ensureDaemon() to auto-start if needed.
130
+ */
131
+
132
+ /**
133
+ * Check if the daemon is currently running and healthy.
134
+ * Returns the daemon info if healthy, null otherwise.
135
+ */
136
+ declare function checkDaemon(): Promise<DaemonInfo | null>;
137
+ /**
138
+ * Ensure the daemon is running. Auto-starts if needed.
139
+ * Returns the daemon info (port, pid).
140
+ */
141
+ declare function ensureDaemon(): Promise<DaemonInfo>;
142
+ /**
143
+ * Stop the running daemon.
144
+ */
145
+ declare function stopDaemon(): Promise<boolean>;
146
+
147
+ /**
148
+ * Daemon HTTP Client
149
+ *
150
+ * Thin client used by MCP server processes to communicate with the daemon
151
+ * over its local HTTP API.
152
+ */
153
+
154
+ declare class DaemonClient {
155
+ private baseUrl;
156
+ constructor(port: number);
157
+ private request;
158
+ health(): Promise<boolean>;
159
+ status(): Promise<DaemonStatus>;
160
+ registerAgent(agentId: string, name?: string): Promise<AgentRegistration>;
161
+ unregisterAgent(agentId: string): Promise<{
162
+ removed: boolean;
163
+ }>;
164
+ /**
165
+ * Long-poll for messages intended for this agent.
166
+ * Will block up to ~30s on the daemon side.
167
+ */
168
+ pollMessages(agentId: string): Promise<RoutedMessage[]>;
169
+ send(to: string, text: string, agentId: string): Promise<SendMessageResponse>;
170
+ getContacts(): Promise<WechatContact[]>;
171
+ }
172
+
173
+ /**
174
+ * Message Router
175
+ *
176
+ * Parses incoming WeChat messages for @agent_name mentions and routes
177
+ * them to the appropriate agent. Messages without a mention go to the
178
+ * default agent.
179
+ */
180
+
181
+ /**
182
+ * Parse @mention from the beginning of a message.
183
+ * Supports: @claude, @openclaw, @paean, etc.
184
+ * The mention must appear at the start (possibly after whitespace).
185
+ */
186
+ declare function parseMention(text: string): {
187
+ agent: string | null;
188
+ rest: string;
189
+ };
190
+
191
+ declare const PACKAGE_VERSION = "0.1.0";
192
+ declare const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
193
+ declare const CONFIG_DIR: string;
194
+
195
+ export { type AccountData, type AgentRegistration, CONFIG_DIR, DEFAULT_BASE_URL, DaemonClient, type DaemonInfo, type DaemonStatus, type GetUpdatesResp, PACKAGE_VERSION, type PollMessagesResponse, type QRCodeResponse, type QRStatusResponse, type RoutedMessage, type SendMessageRequest, type SendMessageResponse, type WechatContact, type WeixinMessage, checkDaemon, ensureDaemon, extractTextFromMessage, fetchQRCode, getContactToken, getUpdates, loadContacts, loadCredentials, parseMention, pollQRStatus, removeCredentials, saveContact, saveCredentials, sendTextMessage, stopDaemon };
package/dist/index.js ADDED
@@ -0,0 +1,443 @@
1
+ // src/wechat-api.ts
2
+ import crypto from "crypto";
3
+
4
+ // src/constants.ts
5
+ import path from "path";
6
+ import os from "os";
7
+ var PACKAGE_VERSION = "0.1.0";
8
+ var DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
9
+ var BOT_TYPE = "3";
10
+ var CHANNEL_VERSION = "0.1.0";
11
+ var MSG_TYPE_BOT = 2;
12
+ var MSG_STATE_FINISH = 2;
13
+ var MSG_ITEM_TEXT = 1;
14
+ var MSG_ITEM_VOICE = 3;
15
+ var LONG_POLL_TIMEOUT_MS = 35e3;
16
+ var QR_POLL_TIMEOUT_MS = 35e3;
17
+ var SEND_TIMEOUT_MS = 15e3;
18
+ var DAEMON_HOST = "127.0.0.1";
19
+ var CONFIG_DIR = path.join(os.homedir(), ".wechat-mcp");
20
+ var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
21
+ var SYNC_BUF_FILE = path.join(CONFIG_DIR, "sync_buf.txt");
22
+ var CONTACTS_FILE = path.join(CONFIG_DIR, "contacts.json");
23
+ var DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
24
+ var DAEMON_INFO_FILE = path.join(CONFIG_DIR, "daemon.json");
25
+
26
+ // src/wechat-api.ts
27
+ function randomWechatUin() {
28
+ const uint32 = crypto.randomBytes(4).readUInt32BE(0);
29
+ return Buffer.from(String(uint32), "utf-8").toString("base64");
30
+ }
31
+ function buildHeaders(token, body) {
32
+ const headers = {
33
+ "Content-Type": "application/json",
34
+ AuthorizationType: "ilink_bot_token",
35
+ "X-WECHAT-UIN": randomWechatUin()
36
+ };
37
+ if (body) {
38
+ headers["Content-Length"] = String(Buffer.byteLength(body, "utf-8"));
39
+ }
40
+ if (token?.trim()) {
41
+ headers.Authorization = `Bearer ${token.trim()}`;
42
+ }
43
+ return headers;
44
+ }
45
+ function normalizeBaseUrl(baseUrl) {
46
+ return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
47
+ }
48
+ async function apiFetch(baseUrl, endpoint, body, token, timeoutMs = 15e3) {
49
+ const url = new URL(endpoint, normalizeBaseUrl(baseUrl)).toString();
50
+ const headers = buildHeaders(token, body);
51
+ const controller = new AbortController();
52
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
53
+ try {
54
+ const res = await fetch(url, {
55
+ method: "POST",
56
+ headers,
57
+ body,
58
+ signal: controller.signal
59
+ });
60
+ clearTimeout(timer);
61
+ const text = await res.text();
62
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${text}`);
63
+ return text;
64
+ } catch (err) {
65
+ clearTimeout(timer);
66
+ throw err;
67
+ }
68
+ }
69
+ async function fetchQRCode(baseUrl) {
70
+ const url = new URL(
71
+ `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(BOT_TYPE)}`,
72
+ normalizeBaseUrl(baseUrl)
73
+ );
74
+ const res = await fetch(url.toString());
75
+ if (!res.ok) throw new Error(`QR fetch failed: ${res.status}`);
76
+ return await res.json();
77
+ }
78
+ async function pollQRStatus(baseUrl, qrcode) {
79
+ const url = new URL(
80
+ `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`,
81
+ normalizeBaseUrl(baseUrl)
82
+ );
83
+ const controller = new AbortController();
84
+ const timer = setTimeout(() => controller.abort(), QR_POLL_TIMEOUT_MS);
85
+ try {
86
+ const res = await fetch(url.toString(), {
87
+ headers: { "iLink-App-ClientVersion": "1" },
88
+ signal: controller.signal
89
+ });
90
+ clearTimeout(timer);
91
+ if (!res.ok) throw new Error(`QR status failed: ${res.status}`);
92
+ return await res.json();
93
+ } catch (err) {
94
+ clearTimeout(timer);
95
+ if (err instanceof Error && err.name === "AbortError") {
96
+ return { status: "wait" };
97
+ }
98
+ throw err;
99
+ }
100
+ }
101
+ async function getUpdates(baseUrl, token, getUpdatesBuf) {
102
+ try {
103
+ const raw = await apiFetch(
104
+ baseUrl,
105
+ "ilink/bot/getupdates",
106
+ JSON.stringify({
107
+ get_updates_buf: getUpdatesBuf,
108
+ base_info: { channel_version: CHANNEL_VERSION }
109
+ }),
110
+ token,
111
+ LONG_POLL_TIMEOUT_MS
112
+ );
113
+ return JSON.parse(raw);
114
+ } catch (err) {
115
+ if (err instanceof Error && err.name === "AbortError") {
116
+ return { ret: 0, msgs: [], get_updates_buf: getUpdatesBuf };
117
+ }
118
+ throw err;
119
+ }
120
+ }
121
+ function generateClientId() {
122
+ return `wechat-mcp:${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
123
+ }
124
+ async function sendTextMessage(baseUrl, token, to, text, contextToken) {
125
+ const clientId = generateClientId();
126
+ await apiFetch(
127
+ baseUrl,
128
+ "ilink/bot/sendmessage",
129
+ JSON.stringify({
130
+ msg: {
131
+ from_user_id: "",
132
+ to_user_id: to,
133
+ client_id: clientId,
134
+ message_type: MSG_TYPE_BOT,
135
+ message_state: MSG_STATE_FINISH,
136
+ item_list: [{ type: MSG_ITEM_TEXT, text_item: { text } }],
137
+ context_token: contextToken
138
+ },
139
+ base_info: { channel_version: CHANNEL_VERSION }
140
+ }),
141
+ token,
142
+ SEND_TIMEOUT_MS
143
+ );
144
+ return clientId;
145
+ }
146
+ function extractTextFromMessage(msg) {
147
+ if (!msg.item_list?.length) return "";
148
+ for (const item of msg.item_list) {
149
+ if (item.type === MSG_ITEM_TEXT && item.text_item?.text) {
150
+ const text = item.text_item.text;
151
+ const ref = item.ref_msg;
152
+ if (!ref) return text;
153
+ const parts = [];
154
+ if (ref.title) parts.push(ref.title);
155
+ if (!parts.length) return text;
156
+ return `[Quote: ${parts.join(" | ")}]
157
+ ${text}`;
158
+ }
159
+ if (item.type === MSG_ITEM_VOICE && item.voice_item?.text) {
160
+ return item.voice_item.text;
161
+ }
162
+ }
163
+ return "";
164
+ }
165
+
166
+ // src/credentials.ts
167
+ import fs from "fs";
168
+ function ensureDir() {
169
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
170
+ }
171
+ function loadCredentials() {
172
+ try {
173
+ if (!fs.existsSync(CREDENTIALS_FILE)) return null;
174
+ return JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf-8"));
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
179
+ function saveCredentials(data) {
180
+ ensureDir();
181
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2), "utf-8");
182
+ try {
183
+ fs.chmodSync(CREDENTIALS_FILE, 384);
184
+ } catch {
185
+ }
186
+ }
187
+ function removeCredentials() {
188
+ for (const file of [CREDENTIALS_FILE, SYNC_BUF_FILE, CONTACTS_FILE]) {
189
+ try {
190
+ if (fs.existsSync(file)) fs.unlinkSync(file);
191
+ } catch {
192
+ }
193
+ }
194
+ }
195
+ function loadContacts() {
196
+ try {
197
+ if (!fs.existsSync(CONTACTS_FILE)) return [];
198
+ return JSON.parse(fs.readFileSync(CONTACTS_FILE, "utf-8"));
199
+ } catch {
200
+ return [];
201
+ }
202
+ }
203
+ function saveContact(userId, contextToken) {
204
+ const contacts = loadContacts();
205
+ const displayName = userId.split("@")[0] || userId;
206
+ const idx = contacts.findIndex((c) => c.userId === userId);
207
+ const entry = {
208
+ userId,
209
+ contextToken,
210
+ lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
211
+ displayName
212
+ };
213
+ if (idx >= 0) contacts[idx] = entry;
214
+ else contacts.push(entry);
215
+ try {
216
+ ensureDir();
217
+ fs.writeFileSync(CONTACTS_FILE, JSON.stringify(contacts, null, 2), "utf-8");
218
+ } catch {
219
+ }
220
+ }
221
+ function getContactToken(userId) {
222
+ const contacts = loadContacts();
223
+ const match = contacts.find(
224
+ (c) => c.userId === userId || c.displayName === userId
225
+ );
226
+ return match?.contextToken ?? null;
227
+ }
228
+ function loadDaemonInfo() {
229
+ try {
230
+ if (!fs.existsSync(DAEMON_INFO_FILE)) return null;
231
+ return JSON.parse(fs.readFileSync(DAEMON_INFO_FILE, "utf-8"));
232
+ } catch {
233
+ return null;
234
+ }
235
+ }
236
+ function removeDaemonInfo() {
237
+ for (const file of [DAEMON_INFO_FILE, DAEMON_PID_FILE]) {
238
+ try {
239
+ if (fs.existsSync(file)) fs.unlinkSync(file);
240
+ } catch {
241
+ }
242
+ }
243
+ }
244
+ function loadDaemonPid() {
245
+ try {
246
+ if (!fs.existsSync(DAEMON_PID_FILE)) return null;
247
+ const raw = fs.readFileSync(DAEMON_PID_FILE, "utf-8").trim();
248
+ const pid = parseInt(raw, 10);
249
+ return isNaN(pid) ? null : pid;
250
+ } catch {
251
+ return null;
252
+ }
253
+ }
254
+
255
+ // src/daemon/lifecycle.ts
256
+ import { spawn } from "child_process";
257
+ import path2 from "path";
258
+ import { fileURLToPath } from "url";
259
+ function getDaemonScript() {
260
+ const thisFile = typeof __filename !== "undefined" ? __filename : fileURLToPath(import.meta.url);
261
+ return path2.resolve(path2.dirname(thisFile), "..", "daemon", "server.js");
262
+ }
263
+ function isProcessAlive(pid) {
264
+ try {
265
+ process.kill(pid, 0);
266
+ return true;
267
+ } catch {
268
+ return false;
269
+ }
270
+ }
271
+ async function healthCheck(port) {
272
+ try {
273
+ const controller = new AbortController();
274
+ const timer = setTimeout(() => controller.abort(), 3e3);
275
+ const res = await fetch(`http://${DAEMON_HOST}:${port}/health`, {
276
+ signal: controller.signal
277
+ });
278
+ clearTimeout(timer);
279
+ if (!res.ok) return false;
280
+ const data = await res.json();
281
+ return data.ok === true;
282
+ } catch {
283
+ return false;
284
+ }
285
+ }
286
+ async function checkDaemon() {
287
+ const info = loadDaemonInfo();
288
+ if (!info) return null;
289
+ if (!isProcessAlive(info.pid)) {
290
+ removeDaemonInfo();
291
+ return null;
292
+ }
293
+ const healthy = await healthCheck(info.port);
294
+ if (!healthy) {
295
+ removeDaemonInfo();
296
+ return null;
297
+ }
298
+ return info;
299
+ }
300
+ async function startDaemonProcess() {
301
+ const script = getDaemonScript();
302
+ const child = spawn(process.execPath, [script], {
303
+ detached: true,
304
+ stdio: "ignore",
305
+ env: { ...process.env }
306
+ });
307
+ child.unref();
308
+ const deadline = Date.now() + 15e3;
309
+ while (Date.now() < deadline) {
310
+ await new Promise((r) => setTimeout(r, 500));
311
+ const info = loadDaemonInfo();
312
+ if (info && isProcessAlive(info.pid)) {
313
+ const healthy = await healthCheck(info.port);
314
+ if (healthy) return info;
315
+ }
316
+ }
317
+ throw new Error("Daemon failed to start within 15 seconds");
318
+ }
319
+ async function ensureDaemon() {
320
+ const existing = await checkDaemon();
321
+ if (existing) return existing;
322
+ return startDaemonProcess();
323
+ }
324
+ async function stopDaemon() {
325
+ const pid = loadDaemonPid();
326
+ if (!pid) return false;
327
+ if (!isProcessAlive(pid)) {
328
+ removeDaemonInfo();
329
+ return false;
330
+ }
331
+ try {
332
+ process.kill(pid, "SIGTERM");
333
+ const deadline = Date.now() + 5e3;
334
+ while (Date.now() < deadline) {
335
+ await new Promise((r) => setTimeout(r, 200));
336
+ if (!isProcessAlive(pid)) break;
337
+ }
338
+ removeDaemonInfo();
339
+ return true;
340
+ } catch {
341
+ removeDaemonInfo();
342
+ return false;
343
+ }
344
+ }
345
+
346
+ // src/client/index.ts
347
+ var DaemonClient = class {
348
+ baseUrl;
349
+ constructor(port) {
350
+ this.baseUrl = `http://${DAEMON_HOST}:${port}`;
351
+ }
352
+ async request(method, path3, body, timeoutMs = 5e3) {
353
+ const controller = new AbortController();
354
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
355
+ try {
356
+ const res = await fetch(`${this.baseUrl}${path3}`, {
357
+ method,
358
+ headers: body ? { "Content-Type": "application/json" } : void 0,
359
+ body: body ? JSON.stringify(body) : void 0,
360
+ signal: controller.signal
361
+ });
362
+ clearTimeout(timer);
363
+ const data = await res.json();
364
+ if (!res.ok) {
365
+ const errMsg = data.error || `HTTP ${res.status}`;
366
+ throw new Error(errMsg);
367
+ }
368
+ return data;
369
+ } catch (err) {
370
+ clearTimeout(timer);
371
+ throw err;
372
+ }
373
+ }
374
+ async health() {
375
+ try {
376
+ const data = await this.request("GET", "/health");
377
+ return data.ok === true;
378
+ } catch {
379
+ return false;
380
+ }
381
+ }
382
+ async status() {
383
+ return this.request("GET", "/status");
384
+ }
385
+ async registerAgent(agentId, name) {
386
+ return this.request("POST", "/agents", { agentId, name });
387
+ }
388
+ async unregisterAgent(agentId) {
389
+ return this.request("DELETE", `/agents/${encodeURIComponent(agentId)}`);
390
+ }
391
+ /**
392
+ * Long-poll for messages intended for this agent.
393
+ * Will block up to ~30s on the daemon side.
394
+ */
395
+ async pollMessages(agentId) {
396
+ const resp = await this.request(
397
+ "GET",
398
+ `/messages/${encodeURIComponent(agentId)}`,
399
+ void 0,
400
+ 35e3
401
+ );
402
+ return resp.messages;
403
+ }
404
+ async send(to, text, agentId) {
405
+ return this.request("POST", "/send", { to, text, agentId });
406
+ }
407
+ async getContacts() {
408
+ const data = await this.request("GET", "/contacts");
409
+ return data.contacts;
410
+ }
411
+ };
412
+
413
+ // src/daemon/router.ts
414
+ import crypto2 from "crypto";
415
+ function parseMention(text) {
416
+ const trimmed = text.trimStart();
417
+ const match = trimmed.match(/^@(\S+)\s*([\s\S]*)$/);
418
+ if (!match) {
419
+ return { agent: null, rest: text };
420
+ }
421
+ return { agent: match[1].toLowerCase(), rest: match[2] || "" };
422
+ }
423
+ export {
424
+ CONFIG_DIR,
425
+ DEFAULT_BASE_URL,
426
+ DaemonClient,
427
+ PACKAGE_VERSION,
428
+ checkDaemon,
429
+ ensureDaemon,
430
+ extractTextFromMessage,
431
+ fetchQRCode,
432
+ getContactToken,
433
+ getUpdates,
434
+ loadContacts,
435
+ loadCredentials,
436
+ parseMention,
437
+ pollQRStatus,
438
+ removeCredentials,
439
+ saveContact,
440
+ saveCredentials,
441
+ sendTextMessage,
442
+ stopDaemon
443
+ };
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@paean-ai/wechat-mcp",
3
+ "version": "0.1.0",
4
+ "description": "WeChat MCP middleware — shared WeChat connection for multiple AI agents with @mention routing",
5
+ "type": "module",
6
+ "bin": {
7
+ "wechat-mcp": "dist/cli.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "LICENSE",
20
+ "README.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "dev": "tsup --watch",
25
+ "typecheck": "tsc --noEmit",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "prepublishOnly": "npm run build"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.27.1",
32
+ "commander": "^13.0.0",
33
+ "qrcode-terminal": "^0.12.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.0.0",
37
+ "tsup": "^8.0.0",
38
+ "typescript": "^5.0.0",
39
+ "vitest": "^3.0.0"
40
+ },
41
+ "engines": {
42
+ "node": ">=18.0.0"
43
+ },
44
+ "keywords": [
45
+ "wechat",
46
+ "mcp",
47
+ "multi-agent",
48
+ "middleware",
49
+ "weixin",
50
+ "chatbot",
51
+ "ai",
52
+ "claude",
53
+ "openclaw",
54
+ "ilink"
55
+ ],
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "git+https://github.com/paean-ai/wechat-mcp.git"
59
+ },
60
+ "homepage": "https://github.com/paean-ai/wechat-mcp#readme",
61
+ "bugs": {
62
+ "url": "https://github.com/paean-ai/wechat-mcp/issues"
63
+ },
64
+ "author": "Paean AI",
65
+ "license": "MIT"
66
+ }