weixin-mcp 1.2.2 → 1.3.1

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,8 @@
1
+ /**
2
+ * Account management commands:
3
+ * npx weixin-mcp accounts list — list all accounts (default)
4
+ * npx weixin-mcp accounts remove <id> — remove a specific account
5
+ * npx weixin-mcp accounts clean — remove duplicate accounts (same userId), keep newest
6
+ * npx weixin-mcp accounts use <id> — print export WEIXIN_ACCOUNT_ID=<id>
7
+ */
8
+ export declare function manageAccounts(args: string[]): Promise<void>;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Account management commands:
3
+ * npx weixin-mcp accounts list — list all accounts (default)
4
+ * npx weixin-mcp accounts remove <id> — remove a specific account
5
+ * npx weixin-mcp accounts clean — remove duplicate accounts (same userId), keep newest
6
+ * npx weixin-mcp accounts use <id> — print export WEIXIN_ACCOUNT_ID=<id>
7
+ */
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { ACCOUNTS_DIR } from "./paths.js";
11
+ function listFiles() {
12
+ try {
13
+ return fs
14
+ .readdirSync(ACCOUNTS_DIR)
15
+ .filter((f) => f.endsWith(".json") && !f.endsWith(".sync.json") && !f.endsWith(".cursor.json"));
16
+ }
17
+ catch {
18
+ return [];
19
+ }
20
+ }
21
+ function loadAccount(file) {
22
+ const accountId = file.replace(".json", "");
23
+ const data = JSON.parse(fs.readFileSync(path.join(ACCOUNTS_DIR, file), "utf-8"));
24
+ return { ...data, accountId };
25
+ }
26
+ function removeAccount(accountId) {
27
+ const filePath = path.join(ACCOUNTS_DIR, `${accountId}.json`);
28
+ const cursorPath = path.join(ACCOUNTS_DIR, `${accountId}.cursor.json`);
29
+ let removed = false;
30
+ if (fs.existsSync(filePath)) {
31
+ fs.unlinkSync(filePath);
32
+ removed = true;
33
+ }
34
+ if (fs.existsSync(cursorPath)) {
35
+ fs.unlinkSync(cursorPath);
36
+ }
37
+ return removed;
38
+ }
39
+ export async function manageAccounts(args) {
40
+ const subcommand = args[0] ?? "list";
41
+ if (subcommand === "list") {
42
+ const files = listFiles();
43
+ if (files.length === 0) {
44
+ console.log("No accounts found. Run: npx weixin-mcp login");
45
+ return;
46
+ }
47
+ const active = process.env.WEIXIN_ACCOUNT_ID ?? files[0].replace(".json", "");
48
+ console.log(`Accounts (${files.length}):\n`);
49
+ for (const file of files) {
50
+ const acc = loadAccount(file);
51
+ const isActive = acc.accountId === active;
52
+ const savedAt = acc.savedAt ? new Date(acc.savedAt).toLocaleString() : "unknown";
53
+ console.log(`${isActive ? "● " : " "}${acc.accountId}`);
54
+ const displayId = acc.userId?.replace(/@im\.wechat(@im\.wechat)+$/, "@im.wechat") ?? "(unknown)";
55
+ console.log(` User: ${displayId}`);
56
+ console.log(` Saved: ${savedAt}`);
57
+ }
58
+ if (files.length > 1) {
59
+ console.log(`\nActive: ${active} (set WEIXIN_ACCOUNT_ID to change)`);
60
+ }
61
+ }
62
+ else if (subcommand === "remove") {
63
+ const accountId = args[1];
64
+ if (!accountId) {
65
+ console.error("Usage: accounts remove <accountId>");
66
+ process.exit(1);
67
+ }
68
+ if (removeAccount(accountId)) {
69
+ console.log(`✅ Removed: ${accountId}`);
70
+ }
71
+ else {
72
+ console.error(`❌ Account not found: ${accountId}`);
73
+ process.exit(1);
74
+ }
75
+ }
76
+ else if (subcommand === "clean") {
77
+ const files = listFiles();
78
+ const byUserId = new Map();
79
+ for (const file of files) {
80
+ const acc = loadAccount(file);
81
+ // Normalize userId: strip duplicate @im.wechat suffix
82
+ const rawId = acc.userId ?? acc.accountId;
83
+ const key = rawId.replace(/@im\.wechat(@im\.wechat)+$/, "@im.wechat");
84
+ if (!byUserId.has(key))
85
+ byUserId.set(key, []);
86
+ byUserId.get(key).push(acc);
87
+ }
88
+ let removed = 0;
89
+ for (const [userId, accounts] of byUserId) {
90
+ if (accounts.length <= 1)
91
+ continue;
92
+ // Sort by savedAt desc — keep newest
93
+ accounts.sort((a, b) => {
94
+ const ta = a.savedAt ? new Date(a.savedAt).getTime() : 0;
95
+ const tb = b.savedAt ? new Date(b.savedAt).getTime() : 0;
96
+ return tb - ta;
97
+ });
98
+ const [keep, ...remove] = accounts;
99
+ console.log(`UserId: ${userId}`);
100
+ console.log(` ✓ Keep: ${keep.accountId} (${keep.savedAt ?? "?"})`);
101
+ for (const acc of remove) {
102
+ removeAccount(acc.accountId);
103
+ console.log(` ✗ Removed: ${acc.accountId} (${acc.savedAt ?? "?"})`);
104
+ removed++;
105
+ }
106
+ }
107
+ if (removed === 0) {
108
+ console.log("✅ No duplicates found.");
109
+ }
110
+ else {
111
+ console.log(`\n✅ Cleaned ${removed} duplicate account(s).`);
112
+ }
113
+ }
114
+ else if (subcommand === "use") {
115
+ const accountId = args[1];
116
+ if (!accountId) {
117
+ console.error("Usage: accounts use <accountId>");
118
+ process.exit(1);
119
+ }
120
+ const filePath = path.join(ACCOUNTS_DIR, `${accountId}.json`);
121
+ if (!fs.existsSync(filePath)) {
122
+ console.error(`❌ Account not found: ${accountId}`);
123
+ process.exit(1);
124
+ }
125
+ console.log(`export WEIXIN_ACCOUNT_ID="${accountId}"`);
126
+ }
127
+ else {
128
+ console.error(`Unknown accounts subcommand: ${subcommand}`);
129
+ console.error("Usage: accounts [list|remove <id>|clean|use <id>]");
130
+ process.exit(1);
131
+ }
132
+ }
package/dist/cli.d.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * weixin-mcp CLI entry point
3
+ * weixin-mcp CLI
4
4
  *
5
- * Usage:
6
- * npx weixin-mcp — start MCP server (stdio)
7
- * npx weixin-mcp loginQR code login
8
- * npx weixin-mcp status show current account status
5
+ * Commands:
6
+ * (no args) Start MCP server in stdio mode (for Claude Desktop)
7
+ * login QR code login
8
+ * status Show account + daemon status
9
+ * start [--port <n>] Start HTTP MCP daemon in background
10
+ * stop Stop daemon
11
+ * restart [--port <n>] Restart daemon
12
+ * logs [-f] Show daemon logs (tail -f with -f flag)
9
13
  */
10
14
  export {};
package/dist/cli.js CHANGED
@@ -1,15 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * weixin-mcp CLI entry point
3
+ * weixin-mcp CLI
4
4
  *
5
- * Usage:
6
- * npx weixin-mcp — start MCP server (stdio)
7
- * npx weixin-mcp loginQR code login
8
- * npx weixin-mcp status show current account status
5
+ * Commands:
6
+ * (no args) Start MCP server in stdio mode (for Claude Desktop)
7
+ * login QR code login
8
+ * status Show account + daemon status
9
+ * start [--port <n>] Start HTTP MCP daemon in background
10
+ * stop Stop daemon
11
+ * restart [--port <n>] Restart daemon
12
+ * logs [-f] Show daemon logs (tail -f with -f flag)
9
13
  */
10
14
  const command = process.argv[2];
11
15
  if (command === "login") {
12
- // Run the login flow
13
16
  const { main } = await import("./login.js");
14
17
  await main();
15
18
  }
@@ -17,16 +20,53 @@ else if (command === "status") {
17
20
  const { showStatus } = await import("./status.js");
18
21
  await showStatus();
19
22
  }
20
- else if (command === undefined || command === "serve" || command === "start") {
21
- // Default: start MCP server
23
+ else if (command === "start") {
24
+ const portArg = process.argv.indexOf("--port");
25
+ const port = portArg !== -1 ? Number(process.argv[portArg + 1]) : undefined;
26
+ const { startDaemon } = await import("./daemon.js");
27
+ await startDaemon(port);
28
+ }
29
+ else if (command === "stop") {
30
+ const { stopDaemon } = await import("./daemon.js");
31
+ stopDaemon();
32
+ }
33
+ else if (command === "restart") {
34
+ const portArg = process.argv.indexOf("--port");
35
+ const port = portArg !== -1 ? Number(process.argv[portArg + 1]) : undefined;
36
+ const { restartDaemon } = await import("./daemon.js");
37
+ await restartDaemon(port);
38
+ }
39
+ else if (command === "logs") {
40
+ const follow = process.argv.includes("-f") || process.argv.includes("--follow");
41
+ const { showLogs } = await import("./daemon.js");
42
+ showLogs(follow);
43
+ }
44
+ else if (command === "accounts") {
45
+ const { manageAccounts } = await import("./accounts.js");
46
+ await manageAccounts(process.argv.slice(3)); // [subcommand, ...args]
47
+ }
48
+ else if (command === undefined || command === "serve") {
49
+ // Default: stdio MCP server (for Claude Desktop integration)
22
50
  await import("./index.js");
23
51
  }
24
52
  else {
25
53
  console.error(`Unknown command: ${command}`);
26
- console.error(`Usage:
27
- npx weixin-mcp Start MCP server
28
- npx weixin-mcp login QR code login
29
- npx weixin-mcp status Show current account status`);
54
+ console.error(`
55
+ Usage: npx weixin-mcp [command]
56
+
57
+ Commands:
58
+ (no args) Start stdio MCP server (Claude Desktop mode)
59
+ login QR code login
60
+ status Show account and daemon status
61
+ start [--port n] Start HTTP MCP daemon in background (default: 3001)
62
+ stop Stop daemon
63
+ restart Restart daemon
64
+ logs [-f] Show daemon logs (-f to follow)
65
+ accounts [list] List all accounts
66
+ accounts remove <id> Remove an account
67
+ accounts clean Remove duplicate accounts (same userId), keep newest
68
+ accounts use <id> Print export command for switching accounts
69
+ `);
30
70
  process.exit(1);
31
71
  }
32
72
  export {};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Daemon management for weixin-mcp HTTP server.
3
+ *
4
+ * Daemon runs as a child process, writing its PID and port to ~/.weixin-mcp/daemon.json.
5
+ * All daemon output is appended to ~/.weixin-mcp/daemon.log.
6
+ */
7
+ interface DaemonInfo {
8
+ pid: number;
9
+ port: number;
10
+ startedAt: string;
11
+ }
12
+ export declare function daemonStatus(): {
13
+ running: boolean;
14
+ info: DaemonInfo | null;
15
+ };
16
+ export declare function startDaemon(port?: number): Promise<void>;
17
+ export declare function stopDaemon(): void;
18
+ export declare function restartDaemon(port?: number): Promise<void>;
19
+ export declare function showLogs(follow?: boolean): void;
20
+ export {};
package/dist/daemon.js ADDED
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Daemon management for weixin-mcp HTTP server.
3
+ *
4
+ * Daemon runs as a child process, writing its PID and port to ~/.weixin-mcp/daemon.json.
5
+ * All daemon output is appended to ~/.weixin-mcp/daemon.log.
6
+ */
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import os from "node:os";
10
+ import { spawn } from "node:child_process";
11
+ import { fileURLToPath } from "node:url";
12
+ const DATA_DIR = path.join(os.homedir(), ".weixin-mcp");
13
+ const PID_FILE = path.join(DATA_DIR, "daemon.json");
14
+ const LOG_FILE = path.join(DATA_DIR, "daemon.log");
15
+ const DEFAULT_PORT = 3001;
16
+ function readDaemonInfo() {
17
+ try {
18
+ return JSON.parse(fs.readFileSync(PID_FILE, "utf-8"));
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ function isRunning(pid) {
25
+ try {
26
+ process.kill(pid, 0);
27
+ return true;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ export function daemonStatus() {
34
+ const info = readDaemonInfo();
35
+ if (!info)
36
+ return { running: false, info: null };
37
+ const running = isRunning(info.pid);
38
+ if (!running) {
39
+ // stale PID file
40
+ try {
41
+ fs.unlinkSync(PID_FILE);
42
+ }
43
+ catch { }
44
+ }
45
+ return { running, info: running ? info : null };
46
+ }
47
+ export async function startDaemon(port = DEFAULT_PORT) {
48
+ const { running, info } = daemonStatus();
49
+ if (running && info) {
50
+ console.log(`⚠️ Daemon already running (pid ${info.pid}, port ${info.port})`);
51
+ return;
52
+ }
53
+ fs.mkdirSync(DATA_DIR, { recursive: true });
54
+ const __filename = fileURLToPath(import.meta.url);
55
+ const __dirname = path.dirname(__filename);
56
+ const serverScript = path.join(__dirname, "server-http.js");
57
+ const logFd = fs.openSync(LOG_FILE, "a");
58
+ const child = spawn(process.execPath, [serverScript, String(port)], {
59
+ detached: true,
60
+ stdio: ["ignore", logFd, logFd],
61
+ env: { ...process.env, WEIXIN_MCP_PORT: String(port) },
62
+ });
63
+ child.unref();
64
+ fs.closeSync(logFd);
65
+ // Wait briefly for the process to start
66
+ await new Promise((r) => setTimeout(r, 800));
67
+ if (!isRunning(child.pid)) {
68
+ console.error("❌ Daemon failed to start. Check logs:");
69
+ console.error(` npx weixin-mcp logs`);
70
+ process.exit(1);
71
+ }
72
+ const daemonInfo = {
73
+ pid: child.pid,
74
+ port,
75
+ startedAt: new Date().toISOString(),
76
+ };
77
+ fs.writeFileSync(PID_FILE, JSON.stringify(daemonInfo, null, 2));
78
+ console.log(`✅ weixin-mcp daemon started`);
79
+ console.log(` PID: ${child.pid}`);
80
+ console.log(` Port: ${port}`);
81
+ console.log(` URL: http://localhost:${port}/mcp`);
82
+ console.log(` Logs: ${LOG_FILE}`);
83
+ }
84
+ export function stopDaemon() {
85
+ const { running, info } = daemonStatus();
86
+ if (!running || !info) {
87
+ console.log("ℹ️ Daemon is not running.");
88
+ return;
89
+ }
90
+ try {
91
+ process.kill(info.pid, "SIGTERM");
92
+ try {
93
+ fs.unlinkSync(PID_FILE);
94
+ }
95
+ catch { }
96
+ console.log(`✅ Daemon stopped (pid ${info.pid})`);
97
+ }
98
+ catch (err) {
99
+ console.error(`❌ Failed to stop daemon: ${String(err)}`);
100
+ }
101
+ }
102
+ export async function restartDaemon(port) {
103
+ const { info } = daemonStatus();
104
+ stopDaemon();
105
+ await new Promise((r) => setTimeout(r, 500));
106
+ await startDaemon(port ?? info?.port ?? DEFAULT_PORT);
107
+ }
108
+ export function showLogs(follow = false) {
109
+ if (!fs.existsSync(LOG_FILE)) {
110
+ console.log("No log file yet. Start the daemon first:");
111
+ console.log(" npx weixin-mcp start");
112
+ return;
113
+ }
114
+ if (follow) {
115
+ const tail = spawn("tail", ["-f", LOG_FILE], { stdio: "inherit" });
116
+ process.on("SIGINT", () => tail.kill());
117
+ }
118
+ else {
119
+ const lines = fs.readFileSync(LOG_FILE, "utf-8").split("\n").slice(-50).join("\n");
120
+ console.log(lines);
121
+ }
122
+ }
package/dist/login.js CHANGED
@@ -76,7 +76,8 @@ export async function main() {
76
76
  // Use bot_id or a random ID as account identifier
77
77
  const accountId = status.ilink_bot_id?.replace("@", "-").replace(".", "-")
78
78
  ?? crypto.randomBytes(6).toString("hex") + "-im-bot";
79
- saveAccount(accountId, token, baseUrl, userId ? `${userId}@im.wechat` : undefined);
79
+ // userId from API already contains @im.wechat suffix — don't append again
80
+ saveAccount(accountId, token, baseUrl, userId ?? undefined);
80
81
  console.log(`\n🎉 Logged in! Account: ${accountId}`);
81
82
  console.log(` UserId: ${userId ?? "(unknown)"}`);
82
83
  console.log("\nYou can now start the MCP server:");
@@ -0,0 +1,7 @@
1
+ /**
2
+ * HTTP MCP server — runs as a daemon process.
3
+ * Spawned by `weixin-mcp start`, listens on a given port.
4
+ *
5
+ * Clients connect via: http://localhost:<port>/mcp
6
+ */
7
+ export {};
@@ -0,0 +1,156 @@
1
+ /**
2
+ * HTTP MCP server — runs as a daemon process.
3
+ * Spawned by `weixin-mcp start`, listens on a given port.
4
+ *
5
+ * Clients connect via: http://localhost:<port>/mcp
6
+ */
7
+ import express from "express";
8
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
11
+ import { randomUUID } from "node:crypto";
12
+ // Reuse the same tool definitions and handlers from the main server
13
+ import { DEFAULT_BASE_URL, getUpdates, getConfig, sendTextMessage, loadCursor, saveCursor, WeixinAuthError, WeixinNetworkError, } from "./api.js";
14
+ import { ACCOUNTS_DIR } from "./paths.js";
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ const port = Number(process.env.WEIXIN_MCP_PORT ?? process.argv[2] ?? 3001);
18
+ function loadAccount() {
19
+ const files = fs.readdirSync(ACCOUNTS_DIR).filter((f) => f.endsWith(".json") && !f.endsWith(".sync.json") && !f.endsWith(".cursor.json"));
20
+ if (files.length === 0)
21
+ throw new Error("No WeChat account. Run: npx weixin-mcp login");
22
+ const accountId = process.env.WEIXIN_ACCOUNT_ID ?? files[0].replace(".json", "");
23
+ const data = JSON.parse(fs.readFileSync(path.join(ACCOUNTS_DIR, `${accountId}.json`), "utf-8"));
24
+ if (!data.token)
25
+ throw new Error(`No token for ${accountId}. Run: npx weixin-mcp login`);
26
+ return { ...data, accountId };
27
+ }
28
+ function assertStr(v, f) {
29
+ if (typeof v !== "string" || !v.trim())
30
+ throw new Error(`"${f}" must be a non-empty string`);
31
+ return v.trim();
32
+ }
33
+ function fmtErr(e) {
34
+ if (e instanceof WeixinAuthError || e instanceof WeixinNetworkError || e instanceof Error)
35
+ return e.message;
36
+ return String(e);
37
+ }
38
+ // ── MCP server factory ─────────────────────────────────────────────────────
39
+ function createMCPServer() {
40
+ const server = new Server({ name: "weixin-mcp", version: "1.2.2" }, { capabilities: { tools: {} } });
41
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
42
+ tools: [
43
+ {
44
+ name: "weixin_send",
45
+ description: "Send a WeChat text message. Pass context_token from a received message to link the reply.",
46
+ inputSchema: {
47
+ type: "object",
48
+ properties: {
49
+ to: { type: "string" },
50
+ text: { type: "string" },
51
+ context_token: { type: "string" },
52
+ },
53
+ required: ["to", "text"],
54
+ },
55
+ },
56
+ {
57
+ name: "weixin_poll",
58
+ description: "Poll for new WeChat messages (cursor-based, no duplicates).",
59
+ inputSchema: {
60
+ type: "object",
61
+ properties: { reset_cursor: { type: "boolean" } },
62
+ },
63
+ },
64
+ {
65
+ name: "weixin_get_config",
66
+ description: "Get user config (typing ticket, etc.).",
67
+ inputSchema: {
68
+ type: "object",
69
+ properties: {
70
+ user_id: { type: "string" },
71
+ context_token: { type: "string" },
72
+ },
73
+ required: ["user_id"],
74
+ },
75
+ },
76
+ ],
77
+ }));
78
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
79
+ const { token, baseUrl = DEFAULT_BASE_URL, accountId } = loadAccount();
80
+ const { name, arguments: args } = req.params;
81
+ try {
82
+ let result;
83
+ if (name === "weixin_send") {
84
+ const a = (args ?? {});
85
+ result = await sendTextMessage(assertStr(a.to, "to"), assertStr(a.text, "text"), token, baseUrl, a.context_token);
86
+ }
87
+ else if (name === "weixin_poll") {
88
+ const { reset_cursor } = (args ?? {});
89
+ const cursor = reset_cursor ? "" : loadCursor(accountId);
90
+ const resp = await getUpdates(token, baseUrl, cursor);
91
+ if (resp.get_updates_buf)
92
+ saveCursor(accountId, resp.get_updates_buf);
93
+ result = resp;
94
+ }
95
+ else if (name === "weixin_get_config") {
96
+ const a = (args ?? {});
97
+ result = await getConfig(assertStr(a.user_id, "user_id"), token, baseUrl, a.context_token);
98
+ }
99
+ else {
100
+ throw new Error(`Unknown tool: ${name}`);
101
+ }
102
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
103
+ }
104
+ catch (err) {
105
+ return { content: [{ type: "text", text: `Error: ${fmtErr(err)}` }], isError: true };
106
+ }
107
+ });
108
+ return server;
109
+ }
110
+ // ── Express HTTP server ────────────────────────────────────────────────────
111
+ const app = express();
112
+ app.use(express.json());
113
+ // Session store for stateful transports
114
+ const sessions = new Map();
115
+ app.post("/mcp", async (req, res) => {
116
+ // Check if this is an existing session
117
+ const sessionId = req.headers["mcp-session-id"];
118
+ let transport = sessionId ? sessions.get(sessionId) : undefined;
119
+ if (!transport) {
120
+ // New session
121
+ const newSessionId = randomUUID();
122
+ transport = new StreamableHTTPServerTransport({
123
+ sessionIdGenerator: () => newSessionId,
124
+ onsessioninitialized: (id) => { sessions.set(id, transport); },
125
+ });
126
+ transport.onclose = () => { sessions.delete(newSessionId); };
127
+ const server = createMCPServer();
128
+ await server.connect(transport);
129
+ }
130
+ await transport.handleRequest(req, res, req.body);
131
+ });
132
+ app.get("/mcp", async (req, res) => {
133
+ const sessionId = req.headers["mcp-session-id"];
134
+ const transport = sessionId ? sessions.get(sessionId) : undefined;
135
+ if (!transport) {
136
+ res.status(404).json({ error: "Session not found" });
137
+ return;
138
+ }
139
+ await transport.handleRequest(req, res);
140
+ });
141
+ app.delete("/mcp", async (req, res) => {
142
+ const sessionId = req.headers["mcp-session-id"];
143
+ const transport = sessionId ? sessions.get(sessionId) : undefined;
144
+ if (!transport) {
145
+ res.status(404).json({ error: "Session not found" });
146
+ return;
147
+ }
148
+ await transport.handleRequest(req, res);
149
+ });
150
+ app.get("/health", (_req, res) => {
151
+ res.json({ status: "ok", port, sessions: sessions.size });
152
+ });
153
+ app.listen(port, () => {
154
+ console.log(`[weixin-mcp] HTTP MCP server listening on port ${port}`);
155
+ console.log(`[weixin-mcp] MCP endpoint: http://localhost:${port}/mcp`);
156
+ });
package/dist/status.js CHANGED
@@ -1,8 +1,21 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { ACCOUNTS_DIR } from "./paths.js";
4
+ import { daemonStatus } from "./daemon.js";
4
5
  export async function showStatus() {
5
6
  console.log("🔍 weixin-mcp status\n");
7
+ // Daemon status
8
+ const { running, info } = daemonStatus();
9
+ if (running && info) {
10
+ console.log(`🟢 Daemon: running (pid ${info.pid}, port ${info.port})`);
11
+ console.log(` URL: http://localhost:${info.port}/mcp`);
12
+ console.log(` Started: ${new Date(info.startedAt).toLocaleString()}`);
13
+ }
14
+ else {
15
+ console.log("⚫ Daemon: not running");
16
+ console.log(" Tip: npx weixin-mcp start");
17
+ }
18
+ console.log();
6
19
  let files;
7
20
  try {
8
21
  files = fs.readdirSync(ACCOUNTS_DIR).filter((f) => f.endsWith(".json") && !f.endsWith(".sync.json") && !f.endsWith(".cursor.json"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weixin-mcp",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
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",
@@ -16,10 +16,12 @@
16
16
  "weixin-login": "dist/weixin-login.js"
17
17
  },
18
18
  "dependencies": {
19
- "@modelcontextprotocol/sdk": "^1.0.0",
19
+ "@modelcontextprotocol/sdk": "^1.27.1",
20
+ "express": "^5.2.1",
20
21
  "qrcode-terminal": "^0.12.0"
21
22
  },
22
23
  "devDependencies": {
24
+ "@types/express": "^5.0.6",
23
25
  "@types/node": "^22.0.0",
24
26
  "typescript": "^5.0.0"
25
27
  }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Account management commands:
3
+ * npx weixin-mcp accounts list — list all accounts (default)
4
+ * npx weixin-mcp accounts remove <id> — remove a specific account
5
+ * npx weixin-mcp accounts clean — remove duplicate accounts (same userId), keep newest
6
+ * npx weixin-mcp accounts use <id> — print export WEIXIN_ACCOUNT_ID=<id>
7
+ */
8
+
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { ACCOUNTS_DIR } from "./paths.js";
12
+
13
+ interface AccountData {
14
+ token?: string;
15
+ baseUrl?: string;
16
+ userId?: string;
17
+ savedAt?: string;
18
+ }
19
+
20
+ function listFiles(): string[] {
21
+ try {
22
+ return fs
23
+ .readdirSync(ACCOUNTS_DIR)
24
+ .filter((f) => f.endsWith(".json") && !f.endsWith(".sync.json") && !f.endsWith(".cursor.json"));
25
+ } catch {
26
+ return [];
27
+ }
28
+ }
29
+
30
+ function loadAccount(file: string): AccountData & { accountId: string } {
31
+ const accountId = file.replace(".json", "");
32
+ const data = JSON.parse(fs.readFileSync(path.join(ACCOUNTS_DIR, file), "utf-8")) as AccountData;
33
+ return { ...data, accountId };
34
+ }
35
+
36
+ function removeAccount(accountId: string) {
37
+ const filePath = path.join(ACCOUNTS_DIR, `${accountId}.json`);
38
+ const cursorPath = path.join(ACCOUNTS_DIR, `${accountId}.cursor.json`);
39
+ let removed = false;
40
+ if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); removed = true; }
41
+ if (fs.existsSync(cursorPath)) { fs.unlinkSync(cursorPath); }
42
+ return removed;
43
+ }
44
+
45
+ export async function manageAccounts(args: string[]) {
46
+ const subcommand = args[0] ?? "list";
47
+
48
+ if (subcommand === "list") {
49
+ const files = listFiles();
50
+ if (files.length === 0) {
51
+ console.log("No accounts found. Run: npx weixin-mcp login");
52
+ return;
53
+ }
54
+ const active = process.env.WEIXIN_ACCOUNT_ID ?? files[0].replace(".json", "");
55
+ console.log(`Accounts (${files.length}):\n`);
56
+ for (const file of files) {
57
+ const acc = loadAccount(file);
58
+ const isActive = acc.accountId === active;
59
+ const savedAt = acc.savedAt ? new Date(acc.savedAt).toLocaleString() : "unknown";
60
+ console.log(`${isActive ? "● " : " "}${acc.accountId}`);
61
+ const displayId = acc.userId?.replace(/@im\.wechat(@im\.wechat)+$/, "@im.wechat") ?? "(unknown)";
62
+ console.log(` User: ${displayId}`);
63
+ console.log(` Saved: ${savedAt}`);
64
+ }
65
+ if (files.length > 1) {
66
+ console.log(`\nActive: ${active} (set WEIXIN_ACCOUNT_ID to change)`);
67
+ }
68
+
69
+ } else if (subcommand === "remove") {
70
+ const accountId = args[1];
71
+ if (!accountId) { console.error("Usage: accounts remove <accountId>"); process.exit(1); }
72
+ if (removeAccount(accountId)) {
73
+ console.log(`✅ Removed: ${accountId}`);
74
+ } else {
75
+ console.error(`❌ Account not found: ${accountId}`);
76
+ process.exit(1);
77
+ }
78
+
79
+ } else if (subcommand === "clean") {
80
+ const files = listFiles();
81
+ const byUserId = new Map<string, Array<AccountData & { accountId: string }>>();
82
+
83
+ for (const file of files) {
84
+ const acc = loadAccount(file);
85
+ // Normalize userId: strip duplicate @im.wechat suffix
86
+ const rawId = acc.userId ?? acc.accountId;
87
+ const key = rawId.replace(/@im\.wechat(@im\.wechat)+$/, "@im.wechat");
88
+ if (!byUserId.has(key)) byUserId.set(key, []);
89
+ byUserId.get(key)!.push(acc);
90
+ }
91
+
92
+ let removed = 0;
93
+ for (const [userId, accounts] of byUserId) {
94
+ if (accounts.length <= 1) continue;
95
+ // Sort by savedAt desc — keep newest
96
+ accounts.sort((a, b) => {
97
+ const ta = a.savedAt ? new Date(a.savedAt).getTime() : 0;
98
+ const tb = b.savedAt ? new Date(b.savedAt).getTime() : 0;
99
+ return tb - ta;
100
+ });
101
+ const [keep, ...remove] = accounts;
102
+ console.log(`UserId: ${userId}`);
103
+ console.log(` ✓ Keep: ${keep.accountId} (${keep.savedAt ?? "?"})`);
104
+ for (const acc of remove) {
105
+ removeAccount(acc.accountId);
106
+ console.log(` ✗ Removed: ${acc.accountId} (${acc.savedAt ?? "?"})`);
107
+ removed++;
108
+ }
109
+ }
110
+
111
+ if (removed === 0) {
112
+ console.log("✅ No duplicates found.");
113
+ } else {
114
+ console.log(`\n✅ Cleaned ${removed} duplicate account(s).`);
115
+ }
116
+
117
+ } else if (subcommand === "use") {
118
+ const accountId = args[1];
119
+ if (!accountId) { console.error("Usage: accounts use <accountId>"); process.exit(1); }
120
+ const filePath = path.join(ACCOUNTS_DIR, `${accountId}.json`);
121
+ if (!fs.existsSync(filePath)) {
122
+ console.error(`❌ Account not found: ${accountId}`);
123
+ process.exit(1);
124
+ }
125
+ console.log(`export WEIXIN_ACCOUNT_ID="${accountId}"`);
126
+
127
+ } else {
128
+ console.error(`Unknown accounts subcommand: ${subcommand}`);
129
+ console.error("Usage: accounts [list|remove <id>|clean|use <id>]");
130
+ process.exit(1);
131
+ }
132
+ }
package/src/cli.ts CHANGED
@@ -1,30 +1,73 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * weixin-mcp CLI entry point
3
+ * weixin-mcp CLI
4
4
  *
5
- * Usage:
6
- * npx weixin-mcp — start MCP server (stdio)
7
- * npx weixin-mcp loginQR code login
8
- * npx weixin-mcp status show current account status
5
+ * Commands:
6
+ * (no args) Start MCP server in stdio mode (for Claude Desktop)
7
+ * login QR code login
8
+ * status Show account + daemon status
9
+ * start [--port <n>] Start HTTP MCP daemon in background
10
+ * stop Stop daemon
11
+ * restart [--port <n>] Restart daemon
12
+ * logs [-f] Show daemon logs (tail -f with -f flag)
9
13
  */
10
14
 
11
15
  const command = process.argv[2];
12
16
 
13
17
  if (command === "login") {
14
- // Run the login flow
15
18
  const { main } = await import("./login.js");
16
19
  await main();
20
+
17
21
  } else if (command === "status") {
18
22
  const { showStatus } = await import("./status.js");
19
23
  await showStatus();
20
- } else if (command === undefined || command === "serve" || command === "start") {
21
- // Default: start MCP server
24
+
25
+ } else if (command === "start") {
26
+ const portArg = process.argv.indexOf("--port");
27
+ const port = portArg !== -1 ? Number(process.argv[portArg + 1]) : undefined;
28
+ const { startDaemon } = await import("./daemon.js");
29
+ await startDaemon(port);
30
+
31
+ } else if (command === "stop") {
32
+ const { stopDaemon } = await import("./daemon.js");
33
+ stopDaemon();
34
+
35
+ } else if (command === "restart") {
36
+ const portArg = process.argv.indexOf("--port");
37
+ const port = portArg !== -1 ? Number(process.argv[portArg + 1]) : undefined;
38
+ const { restartDaemon } = await import("./daemon.js");
39
+ await restartDaemon(port);
40
+
41
+ } else if (command === "logs") {
42
+ const follow = process.argv.includes("-f") || process.argv.includes("--follow");
43
+ const { showLogs } = await import("./daemon.js");
44
+ showLogs(follow);
45
+
46
+ } else if (command === "accounts") {
47
+ const { manageAccounts } = await import("./accounts.js");
48
+ await manageAccounts(process.argv.slice(3)); // [subcommand, ...args]
49
+
50
+ } else if (command === undefined || command === "serve") {
51
+ // Default: stdio MCP server (for Claude Desktop integration)
22
52
  await import("./index.js");
53
+
23
54
  } else {
24
55
  console.error(`Unknown command: ${command}`);
25
- console.error(`Usage:
26
- npx weixin-mcp Start MCP server
27
- npx weixin-mcp login QR code login
28
- npx weixin-mcp status Show current account status`);
56
+ console.error(`
57
+ Usage: npx weixin-mcp [command]
58
+
59
+ Commands:
60
+ (no args) Start stdio MCP server (Claude Desktop mode)
61
+ login QR code login
62
+ status Show account and daemon status
63
+ start [--port n] Start HTTP MCP daemon in background (default: 3001)
64
+ stop Stop daemon
65
+ restart Restart daemon
66
+ logs [-f] Show daemon logs (-f to follow)
67
+ accounts [list] List all accounts
68
+ accounts remove <id> Remove an account
69
+ accounts clean Remove duplicate accounts (same userId), keep newest
70
+ accounts use <id> Print export command for switching accounts
71
+ `);
29
72
  process.exit(1);
30
73
  }
package/src/daemon.ts ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Daemon management for weixin-mcp HTTP server.
3
+ *
4
+ * Daemon runs as a child process, writing its PID and port to ~/.weixin-mcp/daemon.json.
5
+ * All daemon output is appended to ~/.weixin-mcp/daemon.log.
6
+ */
7
+
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import os from "node:os";
11
+ import { spawn } from "node:child_process";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const DATA_DIR = path.join(os.homedir(), ".weixin-mcp");
15
+ const PID_FILE = path.join(DATA_DIR, "daemon.json");
16
+ const LOG_FILE = path.join(DATA_DIR, "daemon.log");
17
+ const DEFAULT_PORT = 3001;
18
+
19
+ interface DaemonInfo {
20
+ pid: number;
21
+ port: number;
22
+ startedAt: string;
23
+ }
24
+
25
+ function readDaemonInfo(): DaemonInfo | null {
26
+ try {
27
+ return JSON.parse(fs.readFileSync(PID_FILE, "utf-8")) as DaemonInfo;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ function isRunning(pid: number): boolean {
34
+ try {
35
+ process.kill(pid, 0);
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ export function daemonStatus(): { running: boolean; info: DaemonInfo | null } {
43
+ const info = readDaemonInfo();
44
+ if (!info) return { running: false, info: null };
45
+ const running = isRunning(info.pid);
46
+ if (!running) {
47
+ // stale PID file
48
+ try { fs.unlinkSync(PID_FILE); } catch {}
49
+ }
50
+ return { running, info: running ? info : null };
51
+ }
52
+
53
+ export async function startDaemon(port = DEFAULT_PORT): Promise<void> {
54
+ const { running, info } = daemonStatus();
55
+ if (running && info) {
56
+ console.log(`⚠️ Daemon already running (pid ${info.pid}, port ${info.port})`);
57
+ return;
58
+ }
59
+
60
+ fs.mkdirSync(DATA_DIR, { recursive: true });
61
+
62
+ const __filename = fileURLToPath(import.meta.url);
63
+ const __dirname = path.dirname(__filename);
64
+ const serverScript = path.join(__dirname, "server-http.js");
65
+
66
+ const logFd = fs.openSync(LOG_FILE, "a");
67
+ const child = spawn(process.execPath, [serverScript, String(port)], {
68
+ detached: true,
69
+ stdio: ["ignore", logFd, logFd],
70
+ env: { ...process.env, WEIXIN_MCP_PORT: String(port) },
71
+ });
72
+
73
+ child.unref();
74
+ fs.closeSync(logFd);
75
+
76
+ // Wait briefly for the process to start
77
+ await new Promise((r) => setTimeout(r, 800));
78
+
79
+ if (!isRunning(child.pid!)) {
80
+ console.error("❌ Daemon failed to start. Check logs:");
81
+ console.error(` npx weixin-mcp logs`);
82
+ process.exit(1);
83
+ }
84
+
85
+ const daemonInfo: DaemonInfo = {
86
+ pid: child.pid!,
87
+ port,
88
+ startedAt: new Date().toISOString(),
89
+ };
90
+ fs.writeFileSync(PID_FILE, JSON.stringify(daemonInfo, null, 2));
91
+
92
+ console.log(`✅ weixin-mcp daemon started`);
93
+ console.log(` PID: ${child.pid}`);
94
+ console.log(` Port: ${port}`);
95
+ console.log(` URL: http://localhost:${port}/mcp`);
96
+ console.log(` Logs: ${LOG_FILE}`);
97
+ }
98
+
99
+ export function stopDaemon(): void {
100
+ const { running, info } = daemonStatus();
101
+ if (!running || !info) {
102
+ console.log("ℹ️ Daemon is not running.");
103
+ return;
104
+ }
105
+
106
+ try {
107
+ process.kill(info.pid, "SIGTERM");
108
+ try { fs.unlinkSync(PID_FILE); } catch {}
109
+ console.log(`✅ Daemon stopped (pid ${info.pid})`);
110
+ } catch (err) {
111
+ console.error(`❌ Failed to stop daemon: ${String(err)}`);
112
+ }
113
+ }
114
+
115
+ export async function restartDaemon(port?: number): Promise<void> {
116
+ const { info } = daemonStatus();
117
+ stopDaemon();
118
+ await new Promise((r) => setTimeout(r, 500));
119
+ await startDaemon(port ?? info?.port ?? DEFAULT_PORT);
120
+ }
121
+
122
+ export function showLogs(follow = false): void {
123
+ if (!fs.existsSync(LOG_FILE)) {
124
+ console.log("No log file yet. Start the daemon first:");
125
+ console.log(" npx weixin-mcp start");
126
+ return;
127
+ }
128
+
129
+ if (follow) {
130
+ const tail = spawn("tail", ["-f", LOG_FILE], { stdio: "inherit" });
131
+ process.on("SIGINT", () => tail.kill());
132
+ } else {
133
+ const lines = fs.readFileSync(LOG_FILE, "utf-8").split("\n").slice(-50).join("\n");
134
+ console.log(lines);
135
+ }
136
+ }
package/src/login.ts CHANGED
@@ -104,7 +104,8 @@ export async function main() {
104
104
  // Use bot_id or a random ID as account identifier
105
105
  const accountId = status.ilink_bot_id?.replace("@", "-").replace(".", "-")
106
106
  ?? crypto.randomBytes(6).toString("hex") + "-im-bot";
107
- saveAccount(accountId, token, baseUrl, userId ? `${userId}@im.wechat` : undefined);
107
+ // userId from API already contains @im.wechat suffix — don't append again
108
+ saveAccount(accountId, token, baseUrl, userId ?? undefined);
108
109
  console.log(`\n🎉 Logged in! Account: ${accountId}`);
109
110
  console.log(` UserId: ${userId ?? "(unknown)"}`);
110
111
  console.log("\nYou can now start the MCP server:");
@@ -0,0 +1,183 @@
1
+ /**
2
+ * HTTP MCP server — runs as a daemon process.
3
+ * Spawned by `weixin-mcp start`, listens on a given port.
4
+ *
5
+ * Clients connect via: http://localhost:<port>/mcp
6
+ */
7
+
8
+ import express from "express";
9
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
10
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
11
+ import {
12
+ CallToolRequestSchema,
13
+ ListToolsRequestSchema,
14
+ } from "@modelcontextprotocol/sdk/types.js";
15
+ import { randomUUID } from "node:crypto";
16
+
17
+ // Reuse the same tool definitions and handlers from the main server
18
+ import {
19
+ DEFAULT_BASE_URL,
20
+ getUpdates,
21
+ getConfig,
22
+ sendTextMessage,
23
+ loadCursor,
24
+ saveCursor,
25
+ WeixinAuthError,
26
+ WeixinNetworkError,
27
+ } from "./api.js";
28
+ import { ACCOUNTS_DIR } from "./paths.js";
29
+ import fs from "node:fs";
30
+ import path from "node:path";
31
+
32
+ const port = Number(process.env.WEIXIN_MCP_PORT ?? process.argv[2] ?? 3001);
33
+
34
+ // ── Account loader ─────────────────────────────────────────────────────────
35
+
36
+ interface AccountData { token?: string; baseUrl?: string; userId?: string }
37
+
38
+ function loadAccount(): AccountData & { accountId: string } {
39
+ const files = fs.readdirSync(ACCOUNTS_DIR).filter(
40
+ (f) => f.endsWith(".json") && !f.endsWith(".sync.json") && !f.endsWith(".cursor.json"),
41
+ );
42
+ if (files.length === 0) throw new Error("No WeChat account. Run: npx weixin-mcp login");
43
+ const accountId = process.env.WEIXIN_ACCOUNT_ID ?? files[0].replace(".json", "");
44
+ const data = JSON.parse(fs.readFileSync(path.join(ACCOUNTS_DIR, `${accountId}.json`), "utf-8")) as AccountData;
45
+ if (!data.token) throw new Error(`No token for ${accountId}. Run: npx weixin-mcp login`);
46
+ return { ...data, accountId };
47
+ }
48
+
49
+ function assertStr(v: unknown, f: string): string {
50
+ if (typeof v !== "string" || !v.trim()) throw new Error(`"${f}" must be a non-empty string`);
51
+ return v.trim();
52
+ }
53
+
54
+ function fmtErr(e: unknown): string {
55
+ if (e instanceof WeixinAuthError || e instanceof WeixinNetworkError || e instanceof Error) return e.message;
56
+ return String(e);
57
+ }
58
+
59
+ // ── MCP server factory ─────────────────────────────────────────────────────
60
+
61
+ function createMCPServer() {
62
+ const server = new Server(
63
+ { name: "weixin-mcp", version: "1.2.2" },
64
+ { capabilities: { tools: {} } },
65
+ );
66
+
67
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
68
+ tools: [
69
+ {
70
+ name: "weixin_send",
71
+ description: "Send a WeChat text message. Pass context_token from a received message to link the reply.",
72
+ inputSchema: {
73
+ type: "object",
74
+ properties: {
75
+ to: { type: "string" },
76
+ text: { type: "string" },
77
+ context_token: { type: "string" },
78
+ },
79
+ required: ["to", "text"],
80
+ },
81
+ },
82
+ {
83
+ name: "weixin_poll",
84
+ description: "Poll for new WeChat messages (cursor-based, no duplicates).",
85
+ inputSchema: {
86
+ type: "object",
87
+ properties: { reset_cursor: { type: "boolean" } },
88
+ },
89
+ },
90
+ {
91
+ name: "weixin_get_config",
92
+ description: "Get user config (typing ticket, etc.).",
93
+ inputSchema: {
94
+ type: "object",
95
+ properties: {
96
+ user_id: { type: "string" },
97
+ context_token: { type: "string" },
98
+ },
99
+ required: ["user_id"],
100
+ },
101
+ },
102
+ ],
103
+ }));
104
+
105
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
106
+ const { token, baseUrl = DEFAULT_BASE_URL, accountId } = loadAccount();
107
+ const { name, arguments: args } = req.params;
108
+ try {
109
+ let result: unknown;
110
+ if (name === "weixin_send") {
111
+ const a = (args ?? {}) as { to?: string; text?: string; context_token?: string };
112
+ result = await sendTextMessage(assertStr(a.to, "to"), assertStr(a.text, "text"), token!, baseUrl, a.context_token);
113
+ } else if (name === "weixin_poll") {
114
+ const { reset_cursor } = (args ?? {}) as { reset_cursor?: boolean };
115
+ const cursor = reset_cursor ? "" : loadCursor(accountId);
116
+ const resp = await getUpdates(token!, baseUrl, cursor);
117
+ if (resp.get_updates_buf) saveCursor(accountId, resp.get_updates_buf);
118
+ result = resp;
119
+ } else if (name === "weixin_get_config") {
120
+ const a = (args ?? {}) as { user_id?: string; context_token?: string };
121
+ result = await getConfig(assertStr(a.user_id, "user_id"), token!, baseUrl, a.context_token);
122
+ } else {
123
+ throw new Error(`Unknown tool: ${name}`);
124
+ }
125
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
126
+ } catch (err) {
127
+ return { content: [{ type: "text", text: `Error: ${fmtErr(err)}` }], isError: true };
128
+ }
129
+ });
130
+
131
+ return server;
132
+ }
133
+
134
+ // ── Express HTTP server ────────────────────────────────────────────────────
135
+
136
+ const app = express();
137
+ app.use(express.json());
138
+
139
+ // Session store for stateful transports
140
+ const sessions = new Map<string, StreamableHTTPServerTransport>();
141
+
142
+ app.post("/mcp", async (req, res) => {
143
+ // Check if this is an existing session
144
+ const sessionId = req.headers["mcp-session-id"] as string | undefined;
145
+ let transport = sessionId ? sessions.get(sessionId) : undefined;
146
+
147
+ if (!transport) {
148
+ // New session
149
+ const newSessionId = randomUUID();
150
+ transport = new StreamableHTTPServerTransport({
151
+ sessionIdGenerator: () => newSessionId,
152
+ onsessioninitialized: (id) => { sessions.set(id, transport!); },
153
+ });
154
+ transport.onclose = () => { sessions.delete(newSessionId); };
155
+ const server = createMCPServer();
156
+ await server.connect(transport);
157
+ }
158
+
159
+ await transport.handleRequest(req, res, req.body);
160
+ });
161
+
162
+ app.get("/mcp", async (req, res) => {
163
+ const sessionId = req.headers["mcp-session-id"] as string | undefined;
164
+ const transport = sessionId ? sessions.get(sessionId) : undefined;
165
+ if (!transport) { res.status(404).json({ error: "Session not found" }); return; }
166
+ await transport.handleRequest(req, res);
167
+ });
168
+
169
+ app.delete("/mcp", async (req, res) => {
170
+ const sessionId = req.headers["mcp-session-id"] as string | undefined;
171
+ const transport = sessionId ? sessions.get(sessionId) : undefined;
172
+ if (!transport) { res.status(404).json({ error: "Session not found" }); return; }
173
+ await transport.handleRequest(req, res);
174
+ });
175
+
176
+ app.get("/health", (_req, res) => {
177
+ res.json({ status: "ok", port, sessions: sessions.size });
178
+ });
179
+
180
+ app.listen(port, () => {
181
+ console.log(`[weixin-mcp] HTTP MCP server listening on port ${port}`);
182
+ console.log(`[weixin-mcp] MCP endpoint: http://localhost:${port}/mcp`);
183
+ });
package/src/status.ts CHANGED
@@ -1,10 +1,23 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { ACCOUNTS_DIR } from "./paths.js";
4
+ import { daemonStatus } from "./daemon.js";
4
5
 
5
6
  export async function showStatus() {
6
7
  console.log("🔍 weixin-mcp status\n");
7
8
 
9
+ // Daemon status
10
+ const { running, info } = daemonStatus();
11
+ if (running && info) {
12
+ console.log(`🟢 Daemon: running (pid ${info.pid}, port ${info.port})`);
13
+ console.log(` URL: http://localhost:${info.port}/mcp`);
14
+ console.log(` Started: ${new Date(info.startedAt).toLocaleString()}`);
15
+ } else {
16
+ console.log("⚫ Daemon: not running");
17
+ console.log(" Tip: npx weixin-mcp start");
18
+ }
19
+ console.log();
20
+
8
21
  let files: string[];
9
22
  try {
10
23
  files = fs.readdirSync(ACCOUNTS_DIR).filter(