weixin-mcp 1.2.1 → 1.3.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/dist/api.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import os from "node:os";
5
4
  export const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
6
5
  const CHANNEL_VERSION = "1.0.2";
7
6
  export class WeixinAuthError extends Error {
@@ -35,10 +34,9 @@ function buildHeaders(token, bodyStr) {
35
34
  };
36
35
  }
37
36
  // ── Cursor persistence ─────────────────────────────────────────────────────
38
- const STATE_DIR = process.env.OPENCLAW_STATE_DIR?.trim() ||
39
- path.join(os.homedir(), ".openclaw");
37
+ import { ACCOUNTS_DIR } from "./paths.js";
40
38
  function cursorPath(accountId) {
41
- return path.join(STATE_DIR, "openclaw-weixin", "accounts", `${accountId}.cursor.json`);
39
+ return path.join(ACCOUNTS_DIR, `${accountId}.cursor.json`);
42
40
  }
43
41
  export function loadCursor(accountId) {
44
42
  try {
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,45 @@ 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 === undefined || command === "serve") {
45
+ // Default: stdio MCP server (for Claude Desktop integration)
22
46
  await import("./index.js");
23
47
  }
24
48
  else {
25
49
  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`);
50
+ console.error(`
51
+ Usage: npx weixin-mcp [command]
52
+
53
+ Commands:
54
+ (no args) Start stdio MCP server (Claude Desktop mode)
55
+ login QR code login
56
+ status Show account and daemon status
57
+ start [--port n] Start HTTP MCP daemon in background (default port: 3001)
58
+ stop Stop daemon
59
+ restart Restart daemon
60
+ logs [-f] Show daemon logs (-f to follow)
61
+ `);
30
62
  process.exit(1);
31
63
  }
32
64
  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/index.js CHANGED
@@ -8,13 +8,10 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
8
8
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
9
9
  import fs from "node:fs";
10
10
  import path from "node:path";
11
- import os from "node:os";
12
11
  import { DEFAULT_BASE_URL, getUpdates, getConfig, sendTextMessage, loadCursor, saveCursor, WeixinAuthError, WeixinNetworkError, } from "./api.js";
12
+ import { ACCOUNTS_DIR } from "./paths.js";
13
13
  // ── Auth / config ──────────────────────────────────────────────────────────
14
- const STATE_DIR = process.env.OPENCLAW_STATE_DIR?.trim() ||
15
- process.env.CLAWDBOT_STATE_DIR?.trim() ||
16
- path.join(os.homedir(), ".openclaw");
17
- const WEIXIN_DIR = path.join(STATE_DIR, "openclaw-weixin", "accounts");
14
+ const WEIXIN_DIR = ACCOUNTS_DIR;
18
15
  function loadAccount() {
19
16
  const files = fs
20
17
  .readdirSync(WEIXIN_DIR)
package/dist/login.d.ts CHANGED
@@ -1,10 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * weixin-login — standalone QR login for weixin-mcp
4
- * Usage: node dist/login.js
3
+ * weixin-login — QR login for weixin-mcp
4
+ * Usage: npx weixin-mcp login
5
5
  *
6
- * Fetches a QR code from Weixin API, renders it in terminal,
7
- * polls for scan confirmation, then saves token to:
8
- * ~/.openclaw/openclaw-weixin/accounts/<accountId>.json
6
+ * Token saved to: ~/.weixin-mcp/accounts/ (or ~/.openclaw/openclaw-weixin/accounts/ if OpenClaw is installed)
9
7
  */
10
8
  export declare function main(): Promise<void>;
package/dist/login.js CHANGED
@@ -1,23 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * weixin-login — standalone QR login for weixin-mcp
4
- * Usage: node dist/login.js
3
+ * weixin-login — QR login for weixin-mcp
4
+ * Usage: npx weixin-mcp login
5
5
  *
6
- * Fetches a QR code from Weixin API, renders it in terminal,
7
- * polls for scan confirmation, then saves token to:
8
- * ~/.openclaw/openclaw-weixin/accounts/<accountId>.json
6
+ * Token saved to: ~/.weixin-mcp/accounts/ (or ~/.openclaw/openclaw-weixin/accounts/ if OpenClaw is installed)
9
7
  */
10
8
  import fs from "node:fs";
11
9
  import path from "node:path";
12
- import os from "node:os";
13
10
  import crypto from "node:crypto";
14
11
  // @ts-ignore — no types for qrcode-terminal
15
12
  import qrcode from "qrcode-terminal";
13
+ import { ACCOUNTS_DIR } from "./paths.js";
16
14
  const BASE_URL = "https://ilinkai.weixin.qq.com";
17
15
  const BOT_TYPE = "3";
18
- const STATE_DIR = process.env.OPENCLAW_STATE_DIR?.trim() ||
19
- path.join(os.homedir(), ".openclaw");
20
- const ACCOUNTS_DIR = path.join(STATE_DIR, "openclaw-weixin", "accounts");
21
16
  async function fetchQRCode() {
22
17
  const base = BASE_URL.endsWith("/") ? BASE_URL : `${BASE_URL}/`;
23
18
  const url = `${base}ilink/bot/get_bot_qrcode?bot_type=${BOT_TYPE}`;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Resolve the accounts directory.
3
+ *
4
+ * Priority:
5
+ * 1. WEIXIN_MCP_DIR env var (explicit override)
6
+ * 2. If OpenClaw state dir exists → ~/.openclaw/openclaw-weixin/accounts/ (backward compat)
7
+ * 3. Default → ~/.weixin-mcp/accounts/
8
+ */
9
+ export declare const ACCOUNTS_DIR: string;
package/dist/paths.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Resolve the accounts directory.
3
+ *
4
+ * Priority:
5
+ * 1. WEIXIN_MCP_DIR env var (explicit override)
6
+ * 2. If OpenClaw state dir exists → ~/.openclaw/openclaw-weixin/accounts/ (backward compat)
7
+ * 3. Default → ~/.weixin-mcp/accounts/
8
+ */
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import os from "node:os";
12
+ function resolveAccountsDir() {
13
+ // Explicit override
14
+ if (process.env.WEIXIN_MCP_DIR?.trim()) {
15
+ return process.env.WEIXIN_MCP_DIR.trim();
16
+ }
17
+ // OpenClaw compat: if the OpenClaw weixin accounts dir exists, use it
18
+ const openclawStateDir = process.env.OPENCLAW_STATE_DIR?.trim() ||
19
+ process.env.CLAWDBOT_STATE_DIR?.trim() ||
20
+ path.join(os.homedir(), ".openclaw");
21
+ const openclawAccountsDir = path.join(openclawStateDir, "openclaw-weixin", "accounts");
22
+ if (fs.existsSync(openclawAccountsDir)) {
23
+ return openclawAccountsDir;
24
+ }
25
+ // Default: standalone ~/.weixin-mcp/accounts/
26
+ return path.join(os.homedir(), ".weixin-mcp", "accounts");
27
+ }
28
+ export const ACCOUNTS_DIR = resolveAccountsDir();
@@ -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,11 +1,21 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import os from "node:os";
4
- const STATE_DIR = process.env.OPENCLAW_STATE_DIR?.trim() ||
5
- path.join(os.homedir(), ".openclaw");
6
- const ACCOUNTS_DIR = path.join(STATE_DIR, "openclaw-weixin", "accounts");
3
+ import { ACCOUNTS_DIR } from "./paths.js";
4
+ import { daemonStatus } from "./daemon.js";
7
5
  export async function showStatus() {
8
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();
9
19
  let files;
10
20
  try {
11
21
  files = fs.readdirSync(ACCOUNTS_DIR).filter((f) => f.endsWith(".json") && !f.endsWith(".sync.json") && !f.endsWith(".cursor.json"));
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weixin-mcp",
3
- "version": "1.2.1",
3
+ "version": "1.3.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",
@@ -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
  }
package/src/api.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import os from "node:os";
5
4
 
6
5
  export const DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com";
7
6
  const CHANNEL_VERSION = "1.0.2";
@@ -44,12 +43,10 @@ function buildHeaders(token: string, bodyStr: string): Record<string, string> {
44
43
 
45
44
  // ── Cursor persistence ─────────────────────────────────────────────────────
46
45
 
47
- const STATE_DIR =
48
- process.env.OPENCLAW_STATE_DIR?.trim() ||
49
- path.join(os.homedir(), ".openclaw");
46
+ import { ACCOUNTS_DIR } from "./paths.js";
50
47
 
51
48
  function cursorPath(accountId: string): string {
52
- return path.join(STATE_DIR, "openclaw-weixin", "accounts", `${accountId}.cursor.json`);
49
+ return path.join(ACCOUNTS_DIR, `${accountId}.cursor.json`);
53
50
  }
54
51
 
55
52
  export function loadCursor(accountId: string): string {
package/src/cli.ts CHANGED
@@ -1,30 +1,65 @@
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 === undefined || command === "serve") {
47
+ // Default: stdio MCP server (for Claude Desktop integration)
22
48
  await import("./index.js");
49
+
23
50
  } else {
24
51
  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`);
52
+ console.error(`
53
+ Usage: npx weixin-mcp [command]
54
+
55
+ Commands:
56
+ (no args) Start stdio MCP server (Claude Desktop mode)
57
+ login QR code login
58
+ status Show account and daemon status
59
+ start [--port n] Start HTTP MCP daemon in background (default port: 3001)
60
+ stop Stop daemon
61
+ restart Restart daemon
62
+ logs [-f] Show daemon logs (-f to follow)
63
+ `);
29
64
  process.exit(1);
30
65
  }
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/index.ts CHANGED
@@ -12,7 +12,6 @@ import {
12
12
  } from "@modelcontextprotocol/sdk/types.js";
13
13
  import fs from "node:fs";
14
14
  import path from "node:path";
15
- import os from "node:os";
16
15
  import {
17
16
  DEFAULT_BASE_URL,
18
17
  getUpdates,
@@ -23,15 +22,11 @@ import {
23
22
  WeixinAuthError,
24
23
  WeixinNetworkError,
25
24
  } from "./api.js";
25
+ import { ACCOUNTS_DIR } from "./paths.js";
26
26
 
27
27
  // ── Auth / config ──────────────────────────────────────────────────────────
28
28
 
29
- const STATE_DIR =
30
- process.env.OPENCLAW_STATE_DIR?.trim() ||
31
- process.env.CLAWDBOT_STATE_DIR?.trim() ||
32
- path.join(os.homedir(), ".openclaw");
33
-
34
- const WEIXIN_DIR = path.join(STATE_DIR, "openclaw-weixin", "accounts");
29
+ const WEIXIN_DIR = ACCOUNTS_DIR;
35
30
 
36
31
  interface AccountData {
37
32
  token?: string;
package/src/login.ts CHANGED
@@ -1,26 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * weixin-login — standalone QR login for weixin-mcp
4
- * Usage: node dist/login.js
3
+ * weixin-login — QR login for weixin-mcp
4
+ * Usage: npx weixin-mcp login
5
5
  *
6
- * Fetches a QR code from Weixin API, renders it in terminal,
7
- * polls for scan confirmation, then saves token to:
8
- * ~/.openclaw/openclaw-weixin/accounts/<accountId>.json
6
+ * Token saved to: ~/.weixin-mcp/accounts/ (or ~/.openclaw/openclaw-weixin/accounts/ if OpenClaw is installed)
9
7
  */
10
8
 
11
9
  import fs from "node:fs";
12
10
  import path from "node:path";
13
- import os from "node:os";
14
11
  import crypto from "node:crypto";
15
12
  // @ts-ignore — no types for qrcode-terminal
16
13
  import qrcode from "qrcode-terminal";
14
+ import { ACCOUNTS_DIR } from "./paths.js";
17
15
 
18
16
  const BASE_URL = "https://ilinkai.weixin.qq.com";
19
17
  const BOT_TYPE = "3";
20
- const STATE_DIR =
21
- process.env.OPENCLAW_STATE_DIR?.trim() ||
22
- path.join(os.homedir(), ".openclaw");
23
- const ACCOUNTS_DIR = path.join(STATE_DIR, "openclaw-weixin", "accounts");
24
18
 
25
19
  async function fetchQRCode(): Promise<{ qrcode: string; qrcode_img_content: string }> {
26
20
  const base = BASE_URL.endsWith("/") ? BASE_URL : `${BASE_URL}/`;
package/src/paths.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Resolve the accounts directory.
3
+ *
4
+ * Priority:
5
+ * 1. WEIXIN_MCP_DIR env var (explicit override)
6
+ * 2. If OpenClaw state dir exists → ~/.openclaw/openclaw-weixin/accounts/ (backward compat)
7
+ * 3. Default → ~/.weixin-mcp/accounts/
8
+ */
9
+
10
+ import fs from "node:fs";
11
+ import path from "node:path";
12
+ import os from "node:os";
13
+
14
+ function resolveAccountsDir(): string {
15
+ // Explicit override
16
+ if (process.env.WEIXIN_MCP_DIR?.trim()) {
17
+ return process.env.WEIXIN_MCP_DIR.trim();
18
+ }
19
+
20
+ // OpenClaw compat: if the OpenClaw weixin accounts dir exists, use it
21
+ const openclawStateDir =
22
+ process.env.OPENCLAW_STATE_DIR?.trim() ||
23
+ process.env.CLAWDBOT_STATE_DIR?.trim() ||
24
+ path.join(os.homedir(), ".openclaw");
25
+
26
+ const openclawAccountsDir = path.join(openclawStateDir, "openclaw-weixin", "accounts");
27
+ if (fs.existsSync(openclawAccountsDir)) {
28
+ return openclawAccountsDir;
29
+ }
30
+
31
+ // Default: standalone ~/.weixin-mcp/accounts/
32
+ return path.join(os.homedir(), ".weixin-mcp", "accounts");
33
+ }
34
+
35
+ export const ACCOUNTS_DIR = resolveAccountsDir();
@@ -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,16 +1,23 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import os from "node:os";
4
-
5
- const STATE_DIR =
6
- process.env.OPENCLAW_STATE_DIR?.trim() ||
7
- path.join(os.homedir(), ".openclaw");
8
-
9
- const ACCOUNTS_DIR = path.join(STATE_DIR, "openclaw-weixin", "accounts");
3
+ import { ACCOUNTS_DIR } from "./paths.js";
4
+ import { daemonStatus } from "./daemon.js";
10
5
 
11
6
  export async function showStatus() {
12
7
  console.log("🔍 weixin-mcp status\n");
13
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
+
14
21
  let files: string[];
15
22
  try {
16
23
  files = fs.readdirSync(ACCOUNTS_DIR).filter(