zalo-agent-cli 1.1.0 → 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/README.md CHANGED
@@ -30,6 +30,28 @@ Xây dựng trên [zca-js](https://github.com/RFS-ADRENO/zca-js).
30
30
  > 15+ nhóm lệnh · listen mode + webhook · 55+ ngân hàng VN · đa tài khoản + proxy
31
31
  > Xem [skill/SKILL.md](skill/SKILL.md) · [Eval scenarios](skill/evals/)
32
32
 
33
+ > [!NOTE]
34
+ > **Zalo Official Account (OA)** — v1.1.0 hỗ trợ Zalo OA API v3.0 chính thức:
35
+ > ```bash
36
+ > zalo-agent oa init # Setup wizard (interactive)
37
+ > zalo-agent oa init --app-id <ID> --secret <KEY> --skip-webhook # Non-interactive (AI agent)
38
+ > zalo-agent oa whoami # Xem thông tin OA
39
+ > zalo-agent oa msg text <user-id> "Xin chào" # Gửi tin nhắn
40
+ > zalo-agent oa listen -p 3000 # Webhook listener
41
+ > ```
42
+ > OAuth login · gửi tin nhắn · quản lý follower · tag · webhook listener · VPS support
43
+ > Xem [docs/official-account.md](docs/official-account.md)
44
+
45
+ > [!TIP]
46
+ > **MCP Server (AI Agent Integration)** — v1.2.0 hỗ trợ Model Context Protocol cho Claude Code và các MCP client:
47
+ > ```bash
48
+ > zalo-agent mcp start # stdio (local Claude Code)
49
+ > zalo-agent mcp start --http 3847 --auth your-secret # HTTP (VPS)
50
+ > ```
51
+ > 4 tools: get_messages · send_message · list_threads · mark_read
52
+ > Auto-reconnect · thread filter · noise reduction · group notifications
53
+ > Xem [MCP Guide](skill/references/mcp-guide.md)
54
+
33
55
  ---
34
56
 
35
57
  ## Cài đặt
@@ -138,6 +160,28 @@ CLI tool for Zalo automation — multi-account, proxy support, bank transfers, Q
138
160
  > 15+ command groups · listen mode + webhook · 55+ VN banks · multi-account + proxy
139
161
  > See [skill/SKILL.md](skill/SKILL.md) · [Eval scenarios](skill/evals/)
140
162
 
163
+ > [!NOTE]
164
+ > **Zalo Official Account (OA)** — v1.1.0 adds official Zalo OA API v3.0:
165
+ > ```bash
166
+ > zalo-agent oa init # Setup wizard (interactive)
167
+ > zalo-agent oa init --app-id <ID> --secret <KEY> --skip-webhook # Non-interactive (AI agent)
168
+ > zalo-agent oa whoami # OA profile
169
+ > zalo-agent oa msg text <user-id> "Hello" # Send message
170
+ > zalo-agent oa listen -p 3000 # Webhook listener
171
+ > ```
172
+ > OAuth login · messaging · follower management · tags · webhook listener · VPS support
173
+ > See [docs/official-account.md](docs/official-account.md)
174
+
175
+ > [!TIP]
176
+ > **MCP Server (AI Agent Integration)** — v1.2.0 adds Model Context Protocol support for Claude Code and MCP clients:
177
+ > ```bash
178
+ > zalo-agent mcp start # stdio (local Claude Code)
179
+ > zalo-agent mcp start --http 3847 --auth your-secret # HTTP (VPS)
180
+ > ```
181
+ > 4 tools: get_messages · send_message · list_threads · mark_read
182
+ > Auto-reconnect · thread filter · noise reduction · group notifications
183
+ > See [MCP Guide](skill/references/mcp-guide.md)
184
+
141
185
  ### Quick Start
142
186
 
143
187
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zalo-agent-cli",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "CLI tool for Zalo automation — multi-account, proxy, bank transfers, QR payments, Official Account API v3.0",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,11 +42,14 @@
42
42
  "node": ">=20"
43
43
  },
44
44
  "dependencies": {
45
+ "@modelcontextprotocol/sdk": "^1.27.1",
45
46
  "chalk": "^5.3.0",
46
47
  "commander": "^14.0.3",
48
+ "express": "^5.2.1",
47
49
  "https-proxy-agent": "^8.0.0",
48
50
  "node-fetch": "^3.3.0",
49
- "zca-js": "^2.1.1"
51
+ "zca-js": "^2.1.1",
52
+ "zod": "^4.3.6"
50
53
  },
51
54
  "devDependencies": {
52
55
  "eslint": "^10.0.3",
package/src/cli.test.js CHANGED
@@ -83,6 +83,7 @@ describe("CLI interface", () => {
83
83
  assert.match(out, /--proxy/);
84
84
  assert.match(out, /--credentials/);
85
85
  assert.match(out, /--qr-url/);
86
+ assert.match(out, /--qr-port/);
86
87
  });
87
88
 
88
89
  it("logout --help shows --purge", () => {
@@ -108,7 +108,21 @@ export function registerListenCommand(program) {
108
108
  if (opts.filter === "group" && msg.type !== THREAD_GROUP) return;
109
109
  if (!opts.self && msg.isSelf) return;
110
110
 
111
- const content = typeof msg.data.content === "string" ? msg.data.content : "[non-text]";
111
+ const rawContent = msg.data.content;
112
+ const isText = typeof rawContent === "string";
113
+ const msgType = msg.data.msgType || null;
114
+ // Build readable display: show type + title/href for non-text
115
+ let displayContent;
116
+ if (isText) {
117
+ displayContent = rawContent;
118
+ } else if (rawContent && typeof rawContent === "object") {
119
+ const parts = [msgType || "attachment"];
120
+ if (rawContent.title) parts.push(`"${rawContent.title}"`);
121
+ if (rawContent.href) parts.push(rawContent.href);
122
+ displayContent = `[${parts.join(" | ")}]`;
123
+ } else {
124
+ displayContent = `[${msgType || "non-text"}]`;
125
+ }
112
126
  const data = {
113
127
  event: "message",
114
128
  msgId: msg.data.msgId,
@@ -116,13 +130,16 @@ export function registerListenCommand(program) {
116
130
  threadId: msg.threadId,
117
131
  type: msg.type,
118
132
  isSelf: msg.isSelf,
119
- content,
133
+ uidFrom: msg.data.uidFrom || null,
134
+ dName: msg.data.dName || null,
135
+ msgType,
136
+ content: rawContent,
120
137
  };
121
138
  const dir = msg.isSelf ? "→" : "←";
122
139
  const typeLabel = msg.type === THREAD_USER ? "DM" : "GR";
123
140
  emitEvent(
124
141
  data,
125
- `${dir} [${typeLabel}] [${msg.threadId}] ${content} (msgId: ${msg.data.msgId})`,
142
+ `${dir} [${typeLabel}] [${msg.threadId}] ${displayContent} (msgId: ${msg.data.msgId})`,
126
143
  );
127
144
  });
128
145
  }
@@ -26,6 +26,7 @@ export function registerLoginCommands(program) {
26
26
  .option("-p, --proxy <url>", "Proxy URL (http/https/socks5://[user:pass@]host:port)")
27
27
  .option("-n, --name <label>", "Friendly name for this account", "")
28
28
  .option("--qr-url", "Start local HTTP server to view QR in browser (for VPS/headless)")
29
+ .option("-q, --qr-port <port>", "Port for QR HTTP server (default: 18927)", parseInt)
29
30
  .option("--credentials <path>", "Login from exported credentials file (skip QR)")
30
31
  .action(async (opts) => {
31
32
  // Credential-based login (headless/CI)
@@ -55,8 +56,9 @@ export function registerLoginCommands(program) {
55
56
  }
56
57
 
57
58
  // QR-based login
59
+ const jsonMode = program.opts().json;
58
60
  if (opts.proxy) info(`Using proxy: ${maskProxy(opts.proxy)}`);
59
- info("Generating QR code... Scan with Zalo mobile app.");
61
+ if (!jsonMode) info("Generating QR code... Scan with Zalo mobile app.");
60
62
 
61
63
  let qrServer = null;
62
64
  try {
@@ -65,7 +67,7 @@ export function registerLoginCommands(program) {
65
67
 
66
68
  // Always start HTTP server for QR scanning (no flag needed)
67
69
  if (!qrServer) {
68
- qrServer = startQrServer(getQRPath());
70
+ qrServer = startQrServer(getQRPath(), opts.qrPort || 18927);
69
71
  }
70
72
  });
71
73
 
@@ -79,9 +81,18 @@ export function registerLoginCommands(program) {
79
81
  const creds = extractCredentials();
80
82
  saveCredentials(ownId, creds);
81
83
  addAccount(ownId, displayName, opts.proxy);
82
- success(`Logged in as ${displayName} (${ownId})`);
84
+
85
+ if (jsonMode) {
86
+ console.log(JSON.stringify({ event: "login_success", ownId, name: displayName }));
87
+ } else {
88
+ success(`Logged in as ${displayName} (${ownId})`);
89
+ }
83
90
  } catch (e) {
84
- error(`Login failed: ${e.message}`);
91
+ if (jsonMode) {
92
+ console.log(JSON.stringify({ event: "login_error", message: e.message }));
93
+ } else {
94
+ error(`Login failed: ${e.message}`);
95
+ }
85
96
  process.exit(1);
86
97
  } finally {
87
98
  if (qrServer) qrServer.close();
@@ -0,0 +1,186 @@
1
+ /**
2
+ * MCP server command — starts a Model Context Protocol server over stdio transport.
3
+ * Allows Claude Code and other MCP clients to read/send Zalo messages via tool calls.
4
+ *
5
+ * IMPORTANT: All diagnostic output uses console.error() — stdout is the MCP transport channel.
6
+ */
7
+
8
+ import { getApi, autoLogin, clearSession } from "../core/zalo-client.js";
9
+ import { MessageBuffer } from "../mcp/message-buffer.js";
10
+ import { ThreadFilter } from "../mcp/thread-filter.js";
11
+ import { loadMCPConfig, parseDuration } from "../mcp/mcp-config.js";
12
+ import { createMCPServer } from "../mcp/mcp-server.js";
13
+ import { registerTools } from "../mcp/mcp-tools.js";
14
+ import { createHTTPServer } from "../mcp/mcp-http-transport.js";
15
+ import { ZaloNotifier } from "../mcp/notifier.js";
16
+
17
+ /** Zalo close code for duplicate web session — fatal, do not retry */
18
+ const CLOSE_DUPLICATE = 3000;
19
+
20
+ /**
21
+ * Normalize a raw zca-js message event into the buffer's message shape.
22
+ * @param {object} msg - Raw zca-js message event
23
+ * @returns {object} Normalized message
24
+ */
25
+ function normalizeMessage(msg) {
26
+ const rawContent = msg.data.content;
27
+ const isText = typeof rawContent === "string";
28
+ return {
29
+ id: msg.data.msgId,
30
+ threadId: msg.threadId,
31
+ threadType: msg.type === 0 ? "dm" : "group",
32
+ senderId: msg.data.uidFrom || null,
33
+ senderName: msg.data.dName || null,
34
+ text: isText
35
+ ? rawContent
36
+ : rawContent?.title || rawContent?.href || `[${msg.data.msgType || "attachment"}]`,
37
+ timestamp: Date.now(),
38
+ type: isText ? "text" : msg.data.msgType || "attachment",
39
+ attachment:
40
+ !isText && rawContent
41
+ ? {
42
+ type: msg.data.msgType,
43
+ url: rawContent.href || null,
44
+ description: rawContent.title || null,
45
+ }
46
+ : null,
47
+ replyTo: null,
48
+ };
49
+ }
50
+
51
+ export function registerMCPCommands(program) {
52
+ const mcp = program.command("mcp").description("MCP server for AI agent integration");
53
+
54
+ mcp.command("start")
55
+ .description("Start MCP server (stdio or HTTP transport)")
56
+ .option("--config <path>", "Config file path (default: ~/.zalo-agent-cli/mcp-config.json)")
57
+ .option("--http <port>", "Use HTTP transport on specified port (default: stdio)")
58
+ .option("--auth <token>", "Bearer token for HTTP auth (only with --http)")
59
+ .action(async (opts) => {
60
+ // Perform login explicitly here — preAction hook skips "mcp"
61
+ try {
62
+ await autoLogin(false);
63
+ } catch (e) {
64
+ console.error("[mcp] Auto-login failed:", e.message);
65
+ process.exit(1);
66
+ }
67
+
68
+ // Load MCP config (config path option reserved for future use)
69
+ const config = loadMCPConfig();
70
+ console.error("[mcp] Config loaded:", JSON.stringify(config.limits));
71
+
72
+ // Build buffer + filter from config
73
+ const maxAge = parseDuration(config.limits?.bufferMaxAge ?? "2h");
74
+ const maxSize = config.limits?.bufferMaxSize ?? 500;
75
+ const buffer = new MessageBuffer(maxSize, maxAge);
76
+ const filter = new ThreadFilter(config);
77
+
78
+ // Start MCP server — stdio (default) or HTTP
79
+ try {
80
+ if (opts.http) {
81
+ const port = Number(opts.http);
82
+ const deps = { api: getApi(), buffer, filter, config };
83
+ createHTTPServer(registerTools, deps, port, opts.auth || null);
84
+ console.error(`[mcp] HTTP server started on port ${port}`);
85
+ } else {
86
+ await createMCPServer(getApi(), buffer, filter, config);
87
+ }
88
+ } catch (e) {
89
+ console.error("[mcp] Failed to start MCP server:", e.message);
90
+ process.exit(1);
91
+ }
92
+
93
+ // Setup notifier (sends to Zalo group when agent is offline)
94
+ const notifier = new ZaloNotifier(getApi(), config);
95
+
96
+ let reconnectCount = 0;
97
+
98
+ /**
99
+ * Attach Zalo listener handlers to the current API instance.
100
+ * Must be called again after each re-login with the new API instance.
101
+ * @param {object} api - zca-js API instance
102
+ */
103
+ function attachListenerHandlers(api) {
104
+ api.listener.on("message", (msg) => {
105
+ // Skip self-sent messages
106
+ if (msg.isSelf) return;
107
+
108
+ const normalized = normalizeMessage(msg);
109
+
110
+ // Apply thread watch filter
111
+ if (!filter.shouldWatch(normalized.threadId, normalized.threadType)) return;
112
+
113
+ // Apply noise filter (stickers, system msgs, short emoji)
114
+ if (!filter.shouldKeep(normalized)) return;
115
+
116
+ buffer.push(normalized.threadId, normalized);
117
+ notifier.onMessage(normalized);
118
+ console.error(
119
+ `[mcp] Buffered ${normalized.threadType} msg from ${normalized.threadId}`,
120
+ );
121
+ });
122
+
123
+ api.listener.on("connected", () => {
124
+ if (reconnectCount > 0) {
125
+ console.error(`[mcp] Reconnected (#${reconnectCount})`);
126
+ }
127
+ });
128
+
129
+ api.listener.on("disconnected", (code) => {
130
+ console.error(`[mcp] Disconnected (code: ${code}). Auto-retrying...`);
131
+ });
132
+
133
+ api.listener.on("closed", async (code) => {
134
+ if (code === CLOSE_DUPLICATE) {
135
+ console.error("[mcp] Duplicate Zalo Web session detected. Exiting.");
136
+ process.exit(1);
137
+ }
138
+ reconnectCount++;
139
+ console.error(
140
+ `[mcp] Connection closed (code: ${code}). Re-login in 5s... (reconnect #${reconnectCount})`,
141
+ );
142
+ await new Promise((r) => setTimeout(r, 5000));
143
+ try {
144
+ clearSession();
145
+ await autoLogin(false);
146
+ console.error("[mcp] Re-login successful. Restarting listener...");
147
+ const newApi = getApi();
148
+ attachListenerHandlers(newApi);
149
+ newApi.listener.start({ retryOnClose: true });
150
+ } catch (e) {
151
+ console.error(`[mcp] Re-login failed: ${e.message}. Retrying in 30s...`);
152
+ await new Promise((r) => setTimeout(r, 30000));
153
+ try {
154
+ clearSession();
155
+ await autoLogin(false);
156
+ const retryApi = getApi();
157
+ attachListenerHandlers(retryApi);
158
+ retryApi.listener.start({ retryOnClose: true });
159
+ console.error("[mcp] Re-login successful on retry.");
160
+ } catch (e2) {
161
+ console.error(`[mcp] Re-login retry failed: ${e2.message}. Exiting.`);
162
+ process.exit(1);
163
+ }
164
+ }
165
+ });
166
+
167
+ api.listener.on("error", () => {
168
+ // WS errors are followed by close/disconnect — suppress to avoid noise
169
+ });
170
+ }
171
+
172
+ // Wire listener and start
173
+ try {
174
+ const api = getApi();
175
+ attachListenerHandlers(api);
176
+ api.listener.start({ retryOnClose: true });
177
+ console.error("[mcp] Zalo listener started. MCP server ready.");
178
+ } catch (e) {
179
+ console.error("[mcp] Failed to start listener:", e.message);
180
+ process.exit(1);
181
+ }
182
+
183
+ // Keep process alive (MCP server runs on stdio — process must not exit)
184
+ await new Promise(() => {});
185
+ });
186
+ }
package/src/index.js CHANGED
@@ -27,6 +27,7 @@ import { registerLabelCommands } from "./commands/label.js";
27
27
  import { registerCatalogCommands } from "./commands/catalog.js";
28
28
  import { registerListenCommand } from "./commands/listen.js";
29
29
  import { registerOACommands } from "./commands/oa.js";
30
+ import { registerMCPCommands } from "./commands/mcp.js";
30
31
  import { autoLogin } from "./core/zalo-client.js";
31
32
  import { checkForUpdates, selfUpdate } from "./utils/update-check.js";
32
33
  import { success, error, warning } from "./utils/output.js";
@@ -44,7 +45,8 @@ program
44
45
  .hook("preAction", async (thisCommand) => {
45
46
  const cmdName = thisCommand.args?.[0] || thisCommand.name();
46
47
  // Suppress zca-js internal logs in JSON mode to keep stdout clean for piping
47
- if (program.opts().json) {
48
+ if (program.opts().json || cmdName === "mcp") {
49
+ // Suppress zca-js stdout logs: JSON mode needs clean output, MCP uses stdout as transport
48
50
  process.env.ZALO_JSON_MODE = "1";
49
51
  } else if (cmdName !== "oa") {
50
52
  // OA commands use official Zalo API — no disclaimer needed
@@ -52,7 +54,7 @@ program
52
54
  console.log();
53
55
  }
54
56
  // Auto-login before any command that needs it (skip for login/account/oa commands)
55
- const skipAutoLogin = ["login", "account", "help", "version", "update", "oa"].includes(cmdName);
57
+ const skipAutoLogin = ["login", "account", "help", "version", "update", "oa", "mcp"].includes(cmdName);
56
58
  if (!skipAutoLogin) {
57
59
  await autoLogin(program.opts().json);
58
60
  }
@@ -88,5 +90,6 @@ registerLabelCommands(program);
88
90
  registerCatalogCommands(program);
89
91
  registerListenCommand(program);
90
92
  registerOACommands(program);
93
+ registerMCPCommands(program);
91
94
 
92
95
  program.parse();
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Load/save MCP-specific config from ~/.zalo-agent-cli/mcp-config.json.
3
+ */
4
+
5
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
6
+ import { join } from "path";
7
+ import { CONFIG_DIR } from "../core/credentials.js";
8
+
9
+ const MCP_CONFIG_FILE = join(CONFIG_DIR, "mcp-config.json");
10
+
11
+ /** Default MCP config — sensible defaults for local development */
12
+ export function getDefaultConfig() {
13
+ return {
14
+ watchThreads: ["dm:*", "group:*"],
15
+ mode: "manual",
16
+ triggerKeywords: ["@bot"],
17
+ notify: {
18
+ enabled: false,
19
+ thread: null,
20
+ on: ["dm"],
21
+ cooldown: "5m",
22
+ },
23
+ limits: {
24
+ maxMessagesPerPoll: 20,
25
+ autoDigestThreshold: 50,
26
+ bufferMaxAge: "2h",
27
+ bufferMaxSize: 500,
28
+ },
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Load MCP config from disk, merged with defaults.
34
+ * @returns {object} MCP config
35
+ */
36
+ export function loadMCPConfig() {
37
+ const defaults = getDefaultConfig();
38
+ try {
39
+ const raw = readFileSync(MCP_CONFIG_FILE, "utf-8");
40
+ const saved = JSON.parse(raw);
41
+ // Shallow merge: saved values override defaults
42
+ return {
43
+ ...defaults,
44
+ ...saved,
45
+ notify: { ...defaults.notify, ...saved.notify },
46
+ limits: { ...defaults.limits, ...saved.limits },
47
+ };
48
+ } catch {
49
+ // File doesn't exist or invalid JSON — use defaults
50
+ return defaults;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Save MCP config to disk.
56
+ * @param {object} config
57
+ */
58
+ export function saveMCPConfig(config) {
59
+ mkdirSync(CONFIG_DIR, { recursive: true });
60
+ writeFileSync(MCP_CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
61
+ }
62
+
63
+ /**
64
+ * Parse duration string (e.g. "2h", "5m", "30s") to milliseconds.
65
+ * @param {string|number} duration
66
+ * @returns {number} Milliseconds
67
+ */
68
+ export function parseDuration(duration) {
69
+ if (typeof duration === "number") return duration;
70
+ const match = String(duration).match(/^(\d+)\s*(h|m|s|ms)?$/i);
71
+ if (!match) return 0;
72
+ const value = parseInt(match[1], 10);
73
+ const unit = (match[2] || "ms").toLowerCase();
74
+ const multipliers = { h: 3600000, m: 60000, s: 1000, ms: 1 };
75
+ return value * (multipliers[unit] || 1);
76
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Express server wrapping MCP StreamableHTTP transport with bearer token auth.
3
+ * Uses stateless mode — each POST /mcp request creates a fresh server+transport.
4
+ */
5
+
6
+ import express from "express";
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
9
+
10
+ /**
11
+ * Create HTTP MCP server with optional bearer token auth.
12
+ * @param {Function} registerToolsFn - Registers tools on a McpServer instance
13
+ * @param {object} deps - { api, buffer, filter, config }
14
+ * @param {number} port
15
+ * @param {string|null} authToken - Bearer token (null = no auth)
16
+ * @returns {import("http").Server}
17
+ */
18
+ export function createHTTPServer(registerToolsFn, deps, port, authToken) {
19
+ const app = express();
20
+ app.use(express.json());
21
+
22
+ // Auth middleware — skips /health
23
+ if (authToken) {
24
+ app.use((req, res, next) => {
25
+ if (req.path === "/health") return next();
26
+ const token = req.headers.authorization?.slice(7); // strip "Bearer "
27
+ if (token !== authToken) {
28
+ return res.status(401).json({ error: "Unauthorized" });
29
+ }
30
+ next();
31
+ });
32
+ }
33
+
34
+ // MCP endpoint — stateless, fresh server+transport per request
35
+ app.post("/mcp", async (req, res) => {
36
+ try {
37
+ const server = new McpServer({ name: "zalo-agent", version: "1.0.0" });
38
+ registerToolsFn(server, deps.api, deps.buffer, deps.filter, deps.config);
39
+
40
+ const transport = new StreamableHTTPServerTransport({
41
+ sessionIdGenerator: undefined, // stateless mode
42
+ });
43
+
44
+ res.on("close", () => {
45
+ transport.close();
46
+ server.close();
47
+ });
48
+
49
+ await server.connect(transport);
50
+ await transport.handleRequest(req, res, req.body);
51
+ } catch (err) {
52
+ console.error("MCP HTTP error:", err);
53
+ if (!res.headersSent) {
54
+ res.status(500).json({
55
+ jsonrpc: "2.0",
56
+ error: { code: -32603, message: "Internal server error" },
57
+ id: null,
58
+ });
59
+ }
60
+ }
61
+ });
62
+
63
+ // Health check — no auth required
64
+ app.get("/health", (req, res) => {
65
+ res.json({
66
+ status: "ok",
67
+ uptime: Math.floor(process.uptime()),
68
+ threads: deps.buffer.getStats().length,
69
+ });
70
+ });
71
+
72
+ return app.listen(port, "0.0.0.0", () => {
73
+ console.error(`MCP HTTP server listening on port ${port}`);
74
+ });
75
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * MCP server setup with stdio transport.
3
+ * Creates and connects the McpServer instance for Claude Code integration.
4
+ */
5
+
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { registerTools } from "./mcp-tools.js";
9
+
10
+ /**
11
+ * Create and start MCP server with stdio transport.
12
+ * All logs MUST use console.error() — stdout is reserved for MCP protocol.
13
+ * @param {object} api - zca-js API instance
14
+ * @param {import("./message-buffer.js").MessageBuffer} buffer
15
+ * @param {import("./thread-filter.js").ThreadFilter} filter
16
+ * @param {object} config - MCP config
17
+ * @returns {Promise<McpServer>}
18
+ */
19
+ export async function createMCPServer(api, buffer, filter, config) {
20
+ const server = new McpServer({
21
+ name: "zalo-agent",
22
+ version: "1.0.0",
23
+ });
24
+
25
+ registerTools(server, api, buffer, filter, config);
26
+
27
+ const transport = new StdioServerTransport();
28
+ await server.connect(transport);
29
+
30
+ console.error("[mcp-server] MCP server connected via stdio transport");
31
+
32
+ return server;
33
+ }