weixin-mcp 1.5.0 → 1.6.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/cli.js CHANGED
@@ -32,8 +32,10 @@ else if (command === "status") {
32
32
  else if (command === "start") {
33
33
  const portArg = process.argv.indexOf("--port");
34
34
  const port = portArg !== -1 ? Number(process.argv[portArg + 1]) : undefined;
35
+ const webhookArg = process.argv.indexOf("--webhook");
36
+ const webhook = webhookArg !== -1 ? process.argv[webhookArg + 1] : undefined;
35
37
  const { startDaemon } = await import("./daemon.js");
36
- await startDaemon(port);
38
+ await startDaemon(port, webhook);
37
39
  }
38
40
  else if (command === "stop") {
39
41
  const { stopDaemon } = await import("./daemon.js");
@@ -113,7 +115,7 @@ Commands:
113
115
  (no args) Start stdio MCP server (Claude Desktop mode)
114
116
  login QR code login
115
117
  status Show account and daemon status
116
- start [--port n] Start HTTP MCP daemon in background (default: 3001)
118
+ start [--port n] [--webhook url] Start HTTP daemon (with optional webhook push)
117
119
  stop Stop daemon
118
120
  restart Restart daemon
119
121
  logs [-f] Show daemon logs (-f to follow)
package/dist/daemon.d.ts CHANGED
@@ -13,7 +13,7 @@ export declare function daemonStatus(): {
13
13
  running: boolean;
14
14
  info: DaemonInfo | null;
15
15
  };
16
- export declare function startDaemon(port?: number): Promise<void>;
16
+ export declare function startDaemon(port?: number, webhook?: string): Promise<void>;
17
17
  export declare function stopDaemon(): void;
18
18
  export declare function restartDaemon(port?: number): Promise<void>;
19
19
  export declare function showLogs(follow?: boolean): void;
package/dist/daemon.js CHANGED
@@ -44,7 +44,7 @@ export function daemonStatus() {
44
44
  }
45
45
  return { running, info: running ? info : null };
46
46
  }
47
- export async function startDaemon(port = DEFAULT_PORT) {
47
+ export async function startDaemon(port = DEFAULT_PORT, webhook) {
48
48
  const { running, info } = daemonStatus();
49
49
  if (running && info) {
50
50
  console.log(`⚠️ Daemon already running (pid ${info.pid}, port ${info.port})`);
@@ -55,10 +55,13 @@ export async function startDaemon(port = DEFAULT_PORT) {
55
55
  const __dirname = path.dirname(__filename);
56
56
  const serverScript = path.join(__dirname, "server-http.js");
57
57
  const logFd = fs.openSync(LOG_FILE, "a");
58
- const child = spawn(process.execPath, [serverScript, String(port)], {
58
+ const serverArgs = ["--port", String(port)];
59
+ if (webhook)
60
+ serverArgs.push("--webhook", webhook);
61
+ const child = spawn(process.execPath, [serverScript, ...serverArgs], {
59
62
  detached: true,
60
63
  stdio: ["ignore", logFd, logFd],
61
- env: { ...process.env, WEIXIN_MCP_PORT: String(port) },
64
+ env: { ...process.env, WEIXIN_MCP_PORT: String(port), WEIXIN_WEBHOOK_URL: webhook ?? "" },
62
65
  });
63
66
  child.unref();
64
67
  fs.closeSync(logFd);
@@ -79,6 +82,8 @@ export async function startDaemon(port = DEFAULT_PORT) {
79
82
  console.log(` PID: ${child.pid}`);
80
83
  console.log(` Port: ${port}`);
81
84
  console.log(` URL: http://localhost:${port}/mcp`);
85
+ if (webhook)
86
+ console.log(` Webhook: ${webhook}`);
82
87
  console.log(` Logs: ${LOG_FILE}`);
83
88
  }
84
89
  export function stopDaemon() {
@@ -2,6 +2,10 @@
2
2
  * HTTP MCP server — runs as a daemon process.
3
3
  * Spawned by `weixin-mcp start`, listens on a given port.
4
4
  *
5
- * Clients connect via: http://localhost:<port>/mcp
5
+ * Features:
6
+ * - MCP endpoint at /mcp (StreamableHTTP)
7
+ * - Health check at /health
8
+ * - Webhook push: --webhook <url> to receive new messages via POST
9
+ * - Auto-poll: when webhook is set, background polling forwards messages
6
10
  */
7
11
  export {};
@@ -2,19 +2,28 @@
2
2
  * HTTP MCP server — runs as a daemon process.
3
3
  * Spawned by `weixin-mcp start`, listens on a given port.
4
4
  *
5
- * Clients connect via: http://localhost:<port>/mcp
5
+ * Features:
6
+ * - MCP endpoint at /mcp (StreamableHTTP)
7
+ * - Health check at /health
8
+ * - Webhook push: --webhook <url> to receive new messages via POST
9
+ * - Auto-poll: when webhook is set, background polling forwards messages
6
10
  */
7
11
  import express from "express";
8
12
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
13
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10
14
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
11
15
  import { randomUUID } from "node:crypto";
12
- // Reuse the same tool definitions and handlers from the main server
13
16
  import { DEFAULT_BASE_URL, getUpdates, getConfig, sendTextMessage, loadCursor, saveCursor, WeixinAuthError, WeixinNetworkError, } from "./api.js";
14
17
  import { ACCOUNTS_DIR } from "./paths.js";
18
+ import { updateContactsFromMsgs, loadContacts } from "./contacts.js";
15
19
  import fs from "node:fs";
16
20
  import path from "node:path";
17
- const port = Number(process.env.WEIXIN_MCP_PORT ?? process.argv[2] ?? 3001);
21
+ // Parse CLI args
22
+ const args = process.argv.slice(2);
23
+ const portIdx = args.indexOf("--port");
24
+ const port = portIdx >= 0 ? Number(args[portIdx + 1]) : Number(process.env.WEIXIN_MCP_PORT ?? 3001);
25
+ const webhookIdx = args.indexOf("--webhook");
26
+ const webhookUrl = webhookIdx >= 0 ? args[webhookIdx + 1] : process.env.WEIXIN_WEBHOOK_URL;
18
27
  function loadAccount() {
19
28
  const files = fs.readdirSync(ACCOUNTS_DIR).filter((f) => f.endsWith(".json") && !f.endsWith(".sync.json") && !f.endsWith(".cursor.json"));
20
29
  if (files.length === 0)
@@ -35,9 +44,18 @@ function fmtErr(e) {
35
44
  return e.message;
36
45
  return String(e);
37
46
  }
47
+ function resolveUserId(input, contacts) {
48
+ if (!input || input.includes("@"))
49
+ return input;
50
+ const ids = Object.keys(contacts);
51
+ const matches = ids.filter((id) => id.startsWith(input) || id.includes(input));
52
+ if (matches.length === 1)
53
+ return matches[0];
54
+ return input;
55
+ }
38
56
  // ── MCP server factory ─────────────────────────────────────────────────────
39
57
  function createMCPServer() {
40
- const server = new Server({ name: "weixin-mcp", version: "1.2.2" }, { capabilities: { tools: {} } });
58
+ const server = new Server({ name: "weixin-mcp", version: "1.5.0" }, { capabilities: { tools: {} } });
41
59
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
42
60
  tools: [
43
61
  {
@@ -46,7 +64,7 @@ function createMCPServer() {
46
64
  inputSchema: {
47
65
  type: "object",
48
66
  properties: {
49
- to: { type: "string" },
67
+ to: { type: "string", description: "Recipient (full ID or short prefix)" },
50
68
  text: { type: "string" },
51
69
  context_token: { type: "string" },
52
70
  },
@@ -61,6 +79,11 @@ function createMCPServer() {
61
79
  properties: { reset_cursor: { type: "boolean" } },
62
80
  },
63
81
  },
82
+ {
83
+ name: "weixin_contacts",
84
+ description: "List users who have messaged the bot.",
85
+ inputSchema: { type: "object", properties: {} },
86
+ },
64
87
  {
65
88
  name: "weixin_get_config",
66
89
  description: "Get user config (typing ticket, etc.).",
@@ -82,7 +105,8 @@ function createMCPServer() {
82
105
  let result;
83
106
  if (name === "weixin_send") {
84
107
  const a = (args ?? {});
85
- result = await sendTextMessage(assertStr(a.to, "to"), assertStr(a.text, "text"), token, baseUrl, a.context_token);
108
+ const resolvedTo = resolveUserId(assertStr(a.to, "to"), loadContacts());
109
+ result = await sendTextMessage(resolvedTo, assertStr(a.text, "text"), token, baseUrl, a.context_token);
86
110
  }
87
111
  else if (name === "weixin_poll") {
88
112
  const { reset_cursor } = (args ?? {});
@@ -90,8 +114,13 @@ function createMCPServer() {
90
114
  const resp = await getUpdates(token, baseUrl, cursor);
91
115
  if (resp.get_updates_buf)
92
116
  saveCursor(accountId, resp.get_updates_buf);
117
+ if (resp.msgs && resp.msgs.length > 0)
118
+ updateContactsFromMsgs(resp.msgs);
93
119
  result = resp;
94
120
  }
121
+ else if (name === "weixin_contacts") {
122
+ result = Object.values(loadContacts());
123
+ }
95
124
  else if (name === "weixin_get_config") {
96
125
  const a = (args ?? {});
97
126
  result = await getConfig(assertStr(a.user_id, "user_id"), token, baseUrl, a.context_token);
@@ -107,17 +136,55 @@ function createMCPServer() {
107
136
  });
108
137
  return server;
109
138
  }
139
+ // ── Webhook push ───────────────────────────────────────────────────────────
140
+ async function pushToWebhook(msgs) {
141
+ if (!webhookUrl || msgs.length === 0)
142
+ return;
143
+ try {
144
+ await fetch(webhookUrl, {
145
+ method: "POST",
146
+ headers: { "Content-Type": "application/json" },
147
+ body: JSON.stringify({ event: "weixin_messages", messages: msgs, timestamp: new Date().toISOString() }),
148
+ });
149
+ }
150
+ catch (err) {
151
+ console.error("[weixin-mcp] webhook push failed:", fmtErr(err));
152
+ }
153
+ }
154
+ // ── Background poller (when webhook is set) ────────────────────────────────
155
+ async function startBackgroundPoller() {
156
+ if (!webhookUrl)
157
+ return;
158
+ console.log(`[weixin-mcp] Webhook enabled: ${webhookUrl}`);
159
+ console.log("[weixin-mcp] Starting background poller...");
160
+ while (true) {
161
+ try {
162
+ const { token, baseUrl = DEFAULT_BASE_URL, accountId } = loadAccount();
163
+ const cursor = loadCursor(accountId);
164
+ const resp = await getUpdates(token, baseUrl, cursor);
165
+ if (resp.get_updates_buf)
166
+ saveCursor(accountId, resp.get_updates_buf);
167
+ if (resp.msgs && resp.msgs.length > 0) {
168
+ updateContactsFromMsgs(resp.msgs);
169
+ await pushToWebhook(resp.msgs);
170
+ console.log(`[weixin-mcp] Pushed ${resp.msgs.length} message(s) to webhook`);
171
+ }
172
+ }
173
+ catch (err) {
174
+ console.error("[weixin-mcp] poll error:", fmtErr(err));
175
+ await new Promise((r) => setTimeout(r, 5000)); // backoff on error
176
+ }
177
+ // getUpdates is long-poll (~30s timeout), so no extra delay needed
178
+ }
179
+ }
110
180
  // ── Express HTTP server ────────────────────────────────────────────────────
111
181
  const app = express();
112
182
  app.use(express.json());
113
- // Session store for stateful transports
114
183
  const sessions = new Map();
115
184
  app.post("/mcp", async (req, res) => {
116
- // Check if this is an existing session
117
185
  const sessionId = req.headers["mcp-session-id"];
118
186
  let transport = sessionId ? sessions.get(sessionId) : undefined;
119
187
  if (!transport) {
120
- // New session
121
188
  const newSessionId = randomUUID();
122
189
  transport = new StreamableHTTPServerTransport({
123
190
  sessionIdGenerator: () => newSessionId,
@@ -148,9 +215,11 @@ app.delete("/mcp", async (req, res) => {
148
215
  await transport.handleRequest(req, res);
149
216
  });
150
217
  app.get("/health", (_req, res) => {
151
- res.json({ status: "ok", port, sessions: sessions.size });
218
+ res.json({ status: "ok", port, sessions: sessions.size, webhook: webhookUrl ?? null });
152
219
  });
153
220
  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`);
221
+ console.log(`[weixin-mcp] HTTP MCP server on port ${port}`);
222
+ console.log(`[weixin-mcp] MCP: http://localhost:${port}/mcp`);
223
+ if (webhookUrl)
224
+ startBackgroundPoller();
156
225
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weixin-mcp",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "MCP server for WeChat (Weixin) — send messages via OpenClaw weixin plugin auth",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/cli.ts CHANGED
@@ -35,8 +35,10 @@ if (command === "login") {
35
35
  } else if (command === "start") {
36
36
  const portArg = process.argv.indexOf("--port");
37
37
  const port = portArg !== -1 ? Number(process.argv[portArg + 1]) : undefined;
38
+ const webhookArg = process.argv.indexOf("--webhook");
39
+ const webhook = webhookArg !== -1 ? process.argv[webhookArg + 1] : undefined;
38
40
  const { startDaemon } = await import("./daemon.js");
39
- await startDaemon(port);
41
+ await startDaemon(port, webhook);
40
42
 
41
43
  } else if (command === "stop") {
42
44
  const { stopDaemon } = await import("./daemon.js");
@@ -115,7 +117,7 @@ Commands:
115
117
  (no args) Start stdio MCP server (Claude Desktop mode)
116
118
  login QR code login
117
119
  status Show account and daemon status
118
- start [--port n] Start HTTP MCP daemon in background (default: 3001)
120
+ start [--port n] [--webhook url] Start HTTP daemon (with optional webhook push)
119
121
  stop Stop daemon
120
122
  restart Restart daemon
121
123
  logs [-f] Show daemon logs (-f to follow)
package/src/daemon.ts CHANGED
@@ -50,7 +50,7 @@ export function daemonStatus(): { running: boolean; info: DaemonInfo | null } {
50
50
  return { running, info: running ? info : null };
51
51
  }
52
52
 
53
- export async function startDaemon(port = DEFAULT_PORT): Promise<void> {
53
+ export async function startDaemon(port = DEFAULT_PORT, webhook?: string): Promise<void> {
54
54
  const { running, info } = daemonStatus();
55
55
  if (running && info) {
56
56
  console.log(`⚠️ Daemon already running (pid ${info.pid}, port ${info.port})`);
@@ -64,10 +64,13 @@ export async function startDaemon(port = DEFAULT_PORT): Promise<void> {
64
64
  const serverScript = path.join(__dirname, "server-http.js");
65
65
 
66
66
  const logFd = fs.openSync(LOG_FILE, "a");
67
- const child = spawn(process.execPath, [serverScript, String(port)], {
67
+ const serverArgs = ["--port", String(port)];
68
+ if (webhook) serverArgs.push("--webhook", webhook);
69
+
70
+ const child = spawn(process.execPath, [serverScript, ...serverArgs], {
68
71
  detached: true,
69
72
  stdio: ["ignore", logFd, logFd],
70
- env: { ...process.env, WEIXIN_MCP_PORT: String(port) },
73
+ env: { ...process.env, WEIXIN_MCP_PORT: String(port), WEIXIN_WEBHOOK_URL: webhook ?? "" },
71
74
  });
72
75
 
73
76
  child.unref();
@@ -93,6 +96,7 @@ export async function startDaemon(port = DEFAULT_PORT): Promise<void> {
93
96
  console.log(` PID: ${child.pid}`);
94
97
  console.log(` Port: ${port}`);
95
98
  console.log(` URL: http://localhost:${port}/mcp`);
99
+ if (webhook) console.log(` Webhook: ${webhook}`);
96
100
  console.log(` Logs: ${LOG_FILE}`);
97
101
  }
98
102
 
@@ -2,7 +2,11 @@
2
2
  * HTTP MCP server — runs as a daemon process.
3
3
  * Spawned by `weixin-mcp start`, listens on a given port.
4
4
  *
5
- * Clients connect via: http://localhost:<port>/mcp
5
+ * Features:
6
+ * - MCP endpoint at /mcp (StreamableHTTP)
7
+ * - Health check at /health
8
+ * - Webhook push: --webhook <url> to receive new messages via POST
9
+ * - Auto-poll: when webhook is set, background polling forwards messages
6
10
  */
7
11
 
8
12
  import express from "express";
@@ -14,7 +18,6 @@ import {
14
18
  } from "@modelcontextprotocol/sdk/types.js";
15
19
  import { randomUUID } from "node:crypto";
16
20
 
17
- // Reuse the same tool definitions and handlers from the main server
18
21
  import {
19
22
  DEFAULT_BASE_URL,
20
23
  getUpdates,
@@ -26,10 +29,16 @@ import {
26
29
  WeixinNetworkError,
27
30
  } from "./api.js";
28
31
  import { ACCOUNTS_DIR } from "./paths.js";
32
+ import { updateContactsFromMsgs, loadContacts, type ContactBook } from "./contacts.js";
29
33
  import fs from "node:fs";
30
34
  import path from "node:path";
31
35
 
32
- const port = Number(process.env.WEIXIN_MCP_PORT ?? process.argv[2] ?? 3001);
36
+ // Parse CLI args
37
+ const args = process.argv.slice(2);
38
+ const portIdx = args.indexOf("--port");
39
+ const port = portIdx >= 0 ? Number(args[portIdx + 1]) : Number(process.env.WEIXIN_MCP_PORT ?? 3001);
40
+ const webhookIdx = args.indexOf("--webhook");
41
+ const webhookUrl = webhookIdx >= 0 ? args[webhookIdx + 1] : process.env.WEIXIN_WEBHOOK_URL;
33
42
 
34
43
  // ── Account loader ─────────────────────────────────────────────────────────
35
44
 
@@ -56,11 +65,19 @@ function fmtErr(e: unknown): string {
56
65
  return String(e);
57
66
  }
58
67
 
68
+ function resolveUserId(input: string, contacts: ContactBook): string {
69
+ if (!input || input.includes("@")) return input;
70
+ const ids = Object.keys(contacts);
71
+ const matches = ids.filter((id) => id.startsWith(input) || id.includes(input));
72
+ if (matches.length === 1) return matches[0];
73
+ return input;
74
+ }
75
+
59
76
  // ── MCP server factory ─────────────────────────────────────────────────────
60
77
 
61
78
  function createMCPServer() {
62
79
  const server = new Server(
63
- { name: "weixin-mcp", version: "1.2.2" },
80
+ { name: "weixin-mcp", version: "1.5.0" },
64
81
  { capabilities: { tools: {} } },
65
82
  );
66
83
 
@@ -72,7 +89,7 @@ function createMCPServer() {
72
89
  inputSchema: {
73
90
  type: "object",
74
91
  properties: {
75
- to: { type: "string" },
92
+ to: { type: "string", description: "Recipient (full ID or short prefix)" },
76
93
  text: { type: "string" },
77
94
  context_token: { type: "string" },
78
95
  },
@@ -87,6 +104,11 @@ function createMCPServer() {
87
104
  properties: { reset_cursor: { type: "boolean" } },
88
105
  },
89
106
  },
107
+ {
108
+ name: "weixin_contacts",
109
+ description: "List users who have messaged the bot.",
110
+ inputSchema: { type: "object", properties: {} },
111
+ },
90
112
  {
91
113
  name: "weixin_get_config",
92
114
  description: "Get user config (typing ticket, etc.).",
@@ -109,13 +131,17 @@ function createMCPServer() {
109
131
  let result: unknown;
110
132
  if (name === "weixin_send") {
111
133
  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);
134
+ const resolvedTo = resolveUserId(assertStr(a.to, "to"), loadContacts());
135
+ result = await sendTextMessage(resolvedTo, assertStr(a.text, "text"), token!, baseUrl, a.context_token);
113
136
  } else if (name === "weixin_poll") {
114
137
  const { reset_cursor } = (args ?? {}) as { reset_cursor?: boolean };
115
138
  const cursor = reset_cursor ? "" : loadCursor(accountId);
116
139
  const resp = await getUpdates(token!, baseUrl, cursor);
117
140
  if (resp.get_updates_buf) saveCursor(accountId, resp.get_updates_buf);
141
+ if (resp.msgs && resp.msgs.length > 0) updateContactsFromMsgs(resp.msgs as unknown[]);
118
142
  result = resp;
143
+ } else if (name === "weixin_contacts") {
144
+ result = Object.values(loadContacts());
119
145
  } else if (name === "weixin_get_config") {
120
146
  const a = (args ?? {}) as { user_id?: string; context_token?: string };
121
147
  result = await getConfig(assertStr(a.user_id, "user_id"), token!, baseUrl, a.context_token);
@@ -131,21 +157,61 @@ function createMCPServer() {
131
157
  return server;
132
158
  }
133
159
 
160
+ // ── Webhook push ───────────────────────────────────────────────────────────
161
+
162
+ async function pushToWebhook(msgs: unknown[]) {
163
+ if (!webhookUrl || msgs.length === 0) return;
164
+ try {
165
+ await fetch(webhookUrl, {
166
+ method: "POST",
167
+ headers: { "Content-Type": "application/json" },
168
+ body: JSON.stringify({ event: "weixin_messages", messages: msgs, timestamp: new Date().toISOString() }),
169
+ });
170
+ } catch (err) {
171
+ console.error("[weixin-mcp] webhook push failed:", fmtErr(err));
172
+ }
173
+ }
174
+
175
+ // ── Background poller (when webhook is set) ────────────────────────────────
176
+
177
+ async function startBackgroundPoller() {
178
+ if (!webhookUrl) return;
179
+ console.log(`[weixin-mcp] Webhook enabled: ${webhookUrl}`);
180
+ console.log("[weixin-mcp] Starting background poller...");
181
+
182
+ while (true) {
183
+ try {
184
+ const { token, baseUrl = DEFAULT_BASE_URL, accountId } = loadAccount();
185
+ const cursor = loadCursor(accountId);
186
+ const resp = await getUpdates(token!, baseUrl, cursor);
187
+
188
+ if (resp.get_updates_buf) saveCursor(accountId, resp.get_updates_buf);
189
+
190
+ if (resp.msgs && resp.msgs.length > 0) {
191
+ updateContactsFromMsgs(resp.msgs as unknown[]);
192
+ await pushToWebhook(resp.msgs);
193
+ console.log(`[weixin-mcp] Pushed ${resp.msgs.length} message(s) to webhook`);
194
+ }
195
+ } catch (err) {
196
+ console.error("[weixin-mcp] poll error:", fmtErr(err));
197
+ await new Promise((r) => setTimeout(r, 5000)); // backoff on error
198
+ }
199
+ // getUpdates is long-poll (~30s timeout), so no extra delay needed
200
+ }
201
+ }
202
+
134
203
  // ── Express HTTP server ────────────────────────────────────────────────────
135
204
 
136
205
  const app = express();
137
206
  app.use(express.json());
138
207
 
139
- // Session store for stateful transports
140
208
  const sessions = new Map<string, StreamableHTTPServerTransport>();
141
209
 
142
210
  app.post("/mcp", async (req, res) => {
143
- // Check if this is an existing session
144
211
  const sessionId = req.headers["mcp-session-id"] as string | undefined;
145
212
  let transport = sessionId ? sessions.get(sessionId) : undefined;
146
213
 
147
214
  if (!transport) {
148
- // New session
149
215
  const newSessionId = randomUUID();
150
216
  transport = new StreamableHTTPServerTransport({
151
217
  sessionIdGenerator: () => newSessionId,
@@ -174,10 +240,11 @@ app.delete("/mcp", async (req, res) => {
174
240
  });
175
241
 
176
242
  app.get("/health", (_req, res) => {
177
- res.json({ status: "ok", port, sessions: sessions.size });
243
+ res.json({ status: "ok", port, sessions: sessions.size, webhook: webhookUrl ?? null });
178
244
  });
179
245
 
180
246
  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`);
247
+ console.log(`[weixin-mcp] HTTP MCP server on port ${port}`);
248
+ console.log(`[weixin-mcp] MCP: http://localhost:${port}/mcp`);
249
+ if (webhookUrl) startBackgroundPoller();
183
250
  });