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 +4 -2
- package/dist/daemon.d.ts +1 -1
- package/dist/daemon.js +8 -3
- package/dist/server-http.d.ts +5 -1
- package/dist/server-http.js +81 -12
- package/package.json +1 -1
- package/src/cli.ts +4 -2
- package/src/daemon.ts +7 -3
- package/src/server-http.ts +79 -12
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]
|
|
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
|
|
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() {
|
package/dist/server-http.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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 {};
|
package/dist/server-http.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
155
|
-
console.log(`[weixin-mcp] 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
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]
|
|
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
|
|
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
|
|
package/src/server-http.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
182
|
-
console.log(`[weixin-mcp] 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
|
});
|