@yoreland/lark-cli-mcp 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 yoreland
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # @yoreland/lark-cli-mcp
2
+
3
+ > One-line `npx` MCP server that lets AI clients (Amazon Quick Desktop, Claude Desktop, …) operate **Feishu / Lark as your own user identity** — send, read, reply, and search messages.
4
+
5
+ It wraps the official [`lark-cli`](https://github.com/larksuite/cli) (bundled as a dependency, no separate install) and exposes 7 messaging tools over MCP stdio. Because every call runs with `--as user`, the sender shown in Feishu is **you**, not a bot.
6
+
7
+ ---
8
+
9
+ ## Quick start (for workshop attendees)
10
+
11
+ ```bash
12
+ # 1) Log in once (OAuth device flow — opens a URL / shows a QR code)
13
+ npx -y @yoreland/lark-cli-mcp auth
14
+
15
+ # 2) Sanity check
16
+ npx -y @yoreland/lark-cli-mcp doctor
17
+ ```
18
+
19
+ Then add the MCP server in your client:
20
+
21
+ | Field | Value |
22
+ | ------------ | ------------------------------ |
23
+ | Connection | Local (stdio) |
24
+ | Command | `npx` |
25
+ | Arguments | `-y @yoreland/lark-cli-mcp` |
26
+
27
+ You should see **7 tools · Connected** ✅
28
+
29
+ > Quick Desktop tip: arguments are space-split. `-y @yoreland/lark-cli-mcp` is fine (no spaces inside the package name).
30
+
31
+ ---
32
+
33
+ ## Prerequisites
34
+
35
+ - **Node.js v18+** (`node -v`)
36
+ - An MCP client (Amazon Quick Desktop / Claude Desktop / etc.)
37
+ - A Feishu/Lark account in the org running the workshop
38
+ - Network access to npm registry and Feishu OAuth
39
+
40
+ The Feishu **App ID / Secret** is provided centrally by the workshop host and baked into the shared `lark-cli` config — attendees only do the OAuth login step. (See [Host setup](#host-setup).)
41
+
42
+ > Behind the Great Firewall? Use a mirror: `npm config set registry https://registry.npmmirror.com`
43
+
44
+ ---
45
+
46
+ ## Commands
47
+
48
+ ```bash
49
+ npx -y @yoreland/lark-cli-mcp # start MCP server (stdio) — what the client runs
50
+ npx -y @yoreland/lark-cli-mcp auth # OAuth device-flow login (user identity)
51
+ npx -y @yoreland/lark-cli-mcp status # show auth status
52
+ npx -y @yoreland/lark-cli-mcp logout # clear token
53
+ npx -y @yoreland/lark-cli-mcp doctor # environment + login self-check
54
+ npx -y @yoreland/lark-cli-mcp -- <args> # passthrough to bundled lark-cli
55
+ ```
56
+
57
+ ---
58
+
59
+ ## The 7 tools
60
+
61
+ | Tool | What it does | lark-cli command |
62
+ | ------------------------ | ----------------------- | -------------------------------------- |
63
+ | `feishu_send_message` | Send a message | `im +messages-send --as user` |
64
+ | `feishu_get_messages` | Read recent messages | `im +chat-messages-list --as user` |
65
+ | `feishu_reply_message` | Reply (thread optional) | `im +messages-reply --as user` |
66
+ | `feishu_search_messages` | Search messages | `im +messages-search --as user` |
67
+ | `feishu_list_chats` | Find group chats | `im +chat-search --as user` |
68
+ | `feishu_search_user` | Find a user (→ open_id) | `contact +search-user --as user` |
69
+ | `feishu_get_thread` | View a thread | `im +threads-messages-list --as user` |
70
+
71
+ ### Talk to it naturally
72
+
73
+ | Goal | Say to your assistant |
74
+ | --------------- | ------------------------------------------- |
75
+ | Read a group | "看看 XX 群最近聊了什么" |
76
+ | Send | "在 XX 群说:明天会议改到 3 点" |
77
+ | Reply | "回复那条消息:收到,我来跟进" |
78
+ | Search | "搜一下谁提过客户报价" |
79
+ | Find someone | "帮我找一下张三的 open_id" |
80
+ | View a thread | "看看那条消息下面的讨论" |
81
+
82
+ ---
83
+
84
+ ## Host setup
85
+
86
+ The workshop host creates **one** Feishu custom app and configures it so attendees share the same App ID/Secret but each authorize their own account.
87
+
88
+ 1. [Feishu Open Platform](https://open.feishu.cn) → create an internal custom app → note **App ID / App Secret**.
89
+ 2. Enable **User token scopes** matching the `im`, `contact`, `search` domains (message read/write, reply, chat read, user search, message search).
90
+ 3. Distribute the App ID/Secret to attendees via `lark-cli config` (or a pre-bound config). The login step requests scopes via `--domain im,contact,search`.
91
+
92
+ `auth` uses **OAuth Device Flow**, so no `redirect URL` / `localhost:3000` callback configuration is required.
93
+
94
+ ---
95
+
96
+ ## Troubleshooting
97
+
98
+ **`missing required scope(s)`** — re-login with the needed domain:
99
+ ```bash
100
+ npx -y @yoreland/lark-cli-mcp auth --domain im,contact,search
101
+ ```
102
+
103
+ **Client shows "No tools loaded"** — run `npx -y @yoreland/lark-cli-mcp doctor`; confirm Node ≥18 and that `auth status` is OK.
104
+
105
+ **Token expired** — just re-run `auth`.
106
+
107
+ ---
108
+
109
+ ## Known limitations
110
+
111
+ - No image/file attachment sending (text + markdown only)
112
+ - No interactive cards
113
+ - No group creation
114
+ - Tokens expire; re-run `auth` when they do
115
+
116
+ ---
117
+
118
+ ## How it works
119
+
120
+ ```
121
+ MCP client (Quick Desktop / Claude Desktop)
122
+ │ stdio (MCP / JSON-RPC)
123
+
124
+ @yoreland/lark-cli-mcp (server.mjs)
125
+ │ child_process.execFile (no shell → injection-safe)
126
+
127
+ lark-cli --as user (bundled dependency)
128
+ │ OAuth user_access_token (device flow)
129
+
130
+ Feishu / Lark Open API
131
+ ```
132
+
133
+ ## License
134
+
135
+ MIT
package/bin/cli.mjs ADDED
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @yoreland/lark-cli-mcp — CLI entry point (npx target)
4
+ *
5
+ * npx @yoreland/lark-cli-mcp # start the MCP server (stdio)
6
+ * npx @yoreland/lark-cli-mcp auth # OAuth device-flow login (as user)
7
+ * npx @yoreland/lark-cli-mcp status # show auth status
8
+ * npx @yoreland/lark-cli-mcp doctor # environment self-check
9
+ * npx @yoreland/lark-cli-mcp -- <args> # passthrough to bundled lark-cli
10
+ *
11
+ * Designed so workshop attendees never install lark-cli separately:
12
+ * the bundled binary ships as a dependency of this package.
13
+ */
14
+ import { spawn } from "node:child_process";
15
+ import { createRequire } from "node:module";
16
+ import { existsSync } from "node:fs";
17
+ import { dirname, join } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+
22
+ // Default scopes for a Feishu messaging workshop (user identity).
23
+ // `im` domain covers send/read/reply; `contact` for user search;
24
+ // `search` for cross-chat message search.
25
+ const DEFAULT_DOMAINS = "im,contact,search";
26
+
27
+ function resolveLarkCli() {
28
+ if (process.env.LARK_CLI_BIN && existsSync(process.env.LARK_CLI_BIN)) {
29
+ return process.env.LARK_CLI_BIN;
30
+ }
31
+ try {
32
+ const require = createRequire(import.meta.url);
33
+ const pkgJson = require.resolve("@larksuite/cli/package.json");
34
+ const binDir = join(dirname(pkgJson), "..", "..", ".bin");
35
+ const candidate = join(
36
+ binDir,
37
+ process.platform === "win32" ? "lark-cli.cmd" : "lark-cli"
38
+ );
39
+ if (existsSync(candidate)) return candidate;
40
+ } catch {
41
+ /* fall through */
42
+ }
43
+ return "lark-cli";
44
+ }
45
+
46
+ const LARK_CLI = resolveLarkCli();
47
+
48
+ function runLark(args, opts = {}) {
49
+ return new Promise((resolve) => {
50
+ const child = spawn(LARK_CLI, args, { stdio: "inherit", ...opts });
51
+ child.on("exit", (code) => resolve(code ?? 0));
52
+ child.on("error", (err) => {
53
+ console.error(`无法运行 lark-cli (${LARK_CLI}): ${err.message}`);
54
+ resolve(1);
55
+ });
56
+ });
57
+ }
58
+
59
+ function startServer() {
60
+ const serverPath = join(__dirname, "..", "server.mjs");
61
+ const child = spawn(process.execPath, [serverPath], { stdio: "inherit" });
62
+ child.on("exit", (code) => process.exit(code ?? 0));
63
+ child.on("error", (err) => {
64
+ console.error(`MCP server 启动失败: ${err.message}`);
65
+ process.exit(1);
66
+ });
67
+ }
68
+
69
+ async function main() {
70
+ const [cmd, ...rest] = process.argv.slice(2);
71
+
72
+ switch (cmd) {
73
+ case undefined:
74
+ case "serve":
75
+ case "start":
76
+ startServer();
77
+ return;
78
+
79
+ case "auth":
80
+ case "login": {
81
+ // Allow override: `... auth --domain im,calendar` or `... auth --scope "..."`
82
+ const hasScopeArg = rest.some(
83
+ (a) => a === "--scope" || a === "--domain" || a === "--recommend"
84
+ );
85
+ const args = ["auth", "login"];
86
+ if (hasScopeArg) args.push(...rest);
87
+ else args.push("--domain", DEFAULT_DOMAINS, ...rest);
88
+ console.error(
89
+ `\n🔑 飞书 OAuth 登录(用户身份,scopes domains: ${
90
+ hasScopeArg ? "(custom)" : DEFAULT_DOMAINS
91
+ })`
92
+ );
93
+ console.error(" 浏览器/二维码授权后即可使用。\n");
94
+ process.exit(await runLark(args));
95
+ return;
96
+ }
97
+
98
+ case "status":
99
+ process.exit(await runLark(["auth", "status", ...rest]));
100
+ return;
101
+
102
+ case "logout":
103
+ process.exit(await runLark(["auth", "logout", ...rest]));
104
+ return;
105
+
106
+ case "doctor": {
107
+ console.log("🩺 lark-cli-mcp 环境自检\n");
108
+ console.log(`Node: ${process.version}`);
109
+ console.log(`Platform: ${process.platform}/${process.arch}`);
110
+ console.log(`lark-cli: ${LARK_CLI}`);
111
+ console.log(` exists: ${existsSync(LARK_CLI) || LARK_CLI === "lark-cli"}`);
112
+ console.log("\n检查登录状态:\n");
113
+ const code = await runLark(["auth", "status"]);
114
+ console.log(
115
+ code === 0
116
+ ? "\n✅ 看起来已登录。现在可在 MCP 客户端用 `npx @yoreland/lark-cli-mcp` 启动。"
117
+ : "\n⚠️ 未登录。请先运行:npx @yoreland/lark-cli-mcp auth"
118
+ );
119
+ process.exit(code);
120
+ return;
121
+ }
122
+
123
+ case "--":
124
+ // Passthrough to bundled lark-cli
125
+ process.exit(await runLark(rest));
126
+ return;
127
+
128
+ case "-h":
129
+ case "--help":
130
+ case "help":
131
+ printHelp();
132
+ return;
133
+
134
+ default:
135
+ // Unknown -> passthrough to lark-cli for power users
136
+ process.exit(await runLark([cmd, ...rest]));
137
+ }
138
+ }
139
+
140
+ function printHelp() {
141
+ console.log(`@yoreland/lark-cli-mcp — Feishu/Lark MCP server (user identity)
142
+
143
+ USAGE
144
+ npx @yoreland/lark-cli-mcp 启动 MCP server (stdio) — 给 MCP 客户端用
145
+ npx @yoreland/lark-cli-mcp auth OAuth 设备码登录(用户身份)
146
+ npx @yoreland/lark-cli-mcp status 查看登录状态
147
+ npx @yoreland/lark-cli-mcp logout 退出登录
148
+ npx @yoreland/lark-cli-mcp doctor 环境与登录自检
149
+ npx @yoreland/lark-cli-mcp -- <args> 透传给底层 lark-cli
150
+
151
+ MCP 客户端配置 (Quick Desktop / Claude Desktop):
152
+ Command: npx
153
+ Arguments: -y @yoreland/lark-cli-mcp
154
+
155
+ 更多: https://github.com/yoreland/lark-cli-mcp`);
156
+ }
157
+
158
+ main();
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@yoreland/lark-cli-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server that wraps lark-cli to operate Feishu/Lark as your own user identity (send/read/reply/search messages). Designed for one-line npx launch.",
5
+ "type": "module",
6
+ "bin": {
7
+ "lark-cli-mcp": "./bin/cli.mjs"
8
+ },
9
+ "main": "./server.mjs",
10
+ "files": [
11
+ "server.mjs",
12
+ "bin/cli.mjs",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "start": "node server.mjs"
17
+ },
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "model-context-protocol",
24
+ "feishu",
25
+ "lark",
26
+ "lark-cli",
27
+ "larksuite"
28
+ ],
29
+ "author": "yoreland",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/yoreland/lark-cli-mcp.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/yoreland/lark-cli-mcp/issues"
37
+ },
38
+ "homepage": "https://github.com/yoreland/lark-cli-mcp#readme",
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "^1.29.0",
41
+ "@larksuite/cli": "^1.0.47"
42
+ }
43
+ }
package/server.mjs ADDED
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @yoreland/lark-cli-mcp — MCP server
4
+ *
5
+ * Wraps the official `lark-cli` (Feishu/Lark CLI) and exposes message
6
+ * operations as MCP tools. All actions run with `--as user`, so messages
7
+ * are sent/read as the logged-in human, not as a bot.
8
+ *
9
+ * The bundled `lark-cli` binary (a dependency of this package) is resolved
10
+ * automatically, so end users only need `npx @yoreland/lark-cli-mcp`.
11
+ */
12
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import {
15
+ CallToolRequestSchema,
16
+ ListToolsRequestSchema,
17
+ } from "@modelcontextprotocol/sdk/types.js";
18
+ import { execFile } from "node:child_process";
19
+ import { promisify } from "node:util";
20
+ import { createRequire } from "node:module";
21
+ import { existsSync } from "node:fs";
22
+ import { dirname, join } from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+
25
+ const execFileAsync = promisify(execFile);
26
+
27
+ /**
28
+ * Resolve the lark-cli executable.
29
+ * Priority:
30
+ * 1. LARK_CLI_BIN env override
31
+ * 2. bundled binary inside this package's node_modules (.bin)
32
+ * 3. fall back to "lark-cli" on PATH (global install)
33
+ */
34
+ function resolveLarkCli() {
35
+ if (process.env.LARK_CLI_BIN && existsSync(process.env.LARK_CLI_BIN)) {
36
+ return process.env.LARK_CLI_BIN;
37
+ }
38
+ try {
39
+ const require = createRequire(import.meta.url);
40
+ // Locate the package, then walk to the .bin shim.
41
+ const pkgJson = require.resolve("@larksuite/cli/package.json");
42
+ const pkgDir = dirname(pkgJson);
43
+ // node_modules/@larksuite/cli -> node_modules/.bin/lark-cli
44
+ const binDir = join(pkgDir, "..", "..", ".bin");
45
+ const candidate = join(
46
+ binDir,
47
+ process.platform === "win32" ? "lark-cli.cmd" : "lark-cli"
48
+ );
49
+ if (existsSync(candidate)) return candidate;
50
+ } catch {
51
+ /* ignore — fall through to PATH */
52
+ }
53
+ return "lark-cli";
54
+ }
55
+
56
+ const LARK_CLI = resolveLarkCli();
57
+
58
+ const server = new Server(
59
+ { name: "lark-cli-mcp", version: "1.0.0" },
60
+ { capabilities: { tools: {} } }
61
+ );
62
+
63
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
64
+ tools: [
65
+ {
66
+ name: "feishu_send_message",
67
+ description:
68
+ "以用户身份发送飞书/Lark 消息到群聊或个人(支持纯文本与 Markdown)",
69
+ inputSchema: {
70
+ type: "object",
71
+ properties: {
72
+ chat_id: { type: "string", description: "群聊 ID(oc_xxx)" },
73
+ user_id: {
74
+ type: "string",
75
+ description: "用户 open_id(ou_xxx),发私聊",
76
+ },
77
+ text: { type: "string", description: "纯文本消息内容" },
78
+ markdown: {
79
+ type: "string",
80
+ description: "Markdown 消息内容(与 text 二选一)",
81
+ },
82
+ },
83
+ },
84
+ },
85
+ {
86
+ name: "feishu_get_messages",
87
+ description: "查看群聊或私聊的最近消息记录",
88
+ inputSchema: {
89
+ type: "object",
90
+ properties: {
91
+ chat_id: { type: "string", description: "群聊 ID(oc_xxx)" },
92
+ user_id: { type: "string", description: "用户 open_id(查私聊)" },
93
+ count: { type: "number", description: "消息数量,默认 20" },
94
+ start_time: { type: "string", description: "起始时间(Unix 秒)" },
95
+ end_time: { type: "string", description: "结束时间(Unix 秒)" },
96
+ },
97
+ },
98
+ },
99
+ {
100
+ name: "feishu_reply_message",
101
+ description: "回复某条飞书消息(支持线程回复)",
102
+ inputSchema: {
103
+ type: "object",
104
+ properties: {
105
+ message_id: { type: "string", description: "消息 ID(om_xxx)" },
106
+ text: { type: "string", description: "回复内容" },
107
+ in_thread: { type: "boolean", description: "是否线程回复" },
108
+ },
109
+ required: ["message_id", "text"],
110
+ },
111
+ },
112
+ {
113
+ name: "feishu_search_messages",
114
+ description: "跨群搜索飞书消息(用户身份)",
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ keyword: { type: "string", description: "搜索关键词" },
119
+ chat_id: { type: "string", description: "限定群(可选)" },
120
+ start_time: { type: "string", description: "起始时间(Unix 秒)" },
121
+ end_time: { type: "string", description: "结束时间(Unix 秒)" },
122
+ },
123
+ required: ["keyword"],
124
+ },
125
+ },
126
+ {
127
+ name: "feishu_list_chats",
128
+ description: "搜索飞书群聊列表(按关键词找 chat_id)",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {
132
+ keyword: { type: "string", description: "搜索关键词(可选)" },
133
+ },
134
+ },
135
+ },
136
+ {
137
+ name: "feishu_search_user",
138
+ description: "按名字/邮箱搜索飞书用户,拿到 open_id",
139
+ inputSchema: {
140
+ type: "object",
141
+ properties: {
142
+ query: { type: "string", description: "用户名字或邮箱" },
143
+ },
144
+ required: ["query"],
145
+ },
146
+ },
147
+ {
148
+ name: "feishu_get_thread",
149
+ description: "查看某条消息的完整线程讨论",
150
+ inputSchema: {
151
+ type: "object",
152
+ properties: {
153
+ message_id: { type: "string", description: "消息/线程 ID(om_/omt_)" },
154
+ count: { type: "number", description: "回复数量,默认 50" },
155
+ },
156
+ required: ["message_id"],
157
+ },
158
+ },
159
+ ],
160
+ }));
161
+
162
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
163
+ const { name, arguments: args = {} } = request.params;
164
+ try {
165
+ switch (name) {
166
+ case "feishu_send_message": {
167
+ const { chat_id, user_id, text, markdown } = args;
168
+ if (!chat_id && !user_id) return error("需要 chat_id 或 user_id");
169
+ if (!text && !markdown) return error("需要 text 或 markdown");
170
+ const cmd = ["im", "+messages-send", "--as", "user"];
171
+ if (chat_id) cmd.push("--chat-id", chat_id);
172
+ else cmd.push("--user-id", user_id);
173
+ if (markdown) cmd.push("--markdown", markdown);
174
+ else cmd.push("--text", text);
175
+ return await run(cmd);
176
+ }
177
+ case "feishu_get_messages": {
178
+ const { chat_id, user_id, count, start_time, end_time } = args;
179
+ if (!chat_id && !user_id) return error("需要 chat_id 或 user_id");
180
+ const cmd = ["im", "+chat-messages-list", "--as", "user"];
181
+ if (chat_id) cmd.push("--chat-id", chat_id);
182
+ else cmd.push("--user-id", user_id);
183
+ if (count) cmd.push("--page-size", String(count));
184
+ if (start_time) cmd.push("--start-time", start_time);
185
+ if (end_time) cmd.push("--end-time", end_time);
186
+ return await run(cmd);
187
+ }
188
+ case "feishu_reply_message": {
189
+ const { message_id, text, in_thread } = args;
190
+ const cmd = [
191
+ "im",
192
+ "+messages-reply",
193
+ "--as",
194
+ "user",
195
+ "--message-id",
196
+ message_id,
197
+ "--text",
198
+ text,
199
+ ];
200
+ if (in_thread === true) cmd.push("--reply-in-thread");
201
+ return await run(cmd);
202
+ }
203
+ case "feishu_search_messages": {
204
+ const { keyword, chat_id, start_time, end_time } = args;
205
+ const cmd = [
206
+ "im",
207
+ "+messages-search",
208
+ "--as",
209
+ "user",
210
+ "--query",
211
+ keyword,
212
+ ];
213
+ if (chat_id) cmd.push("--chat-id", chat_id);
214
+ if (start_time) cmd.push("--start-time", start_time);
215
+ if (end_time) cmd.push("--end-time", end_time);
216
+ return await run(cmd);
217
+ }
218
+ case "feishu_list_chats": {
219
+ const { keyword } = args;
220
+ const cmd = ["im", "+chat-search", "--as", "user"];
221
+ if (keyword) cmd.push("--query", keyword);
222
+ return await run(cmd);
223
+ }
224
+ case "feishu_search_user": {
225
+ const { query } = args;
226
+ return await run([
227
+ "contact",
228
+ "+search-user",
229
+ "--as",
230
+ "user",
231
+ "--query",
232
+ query,
233
+ ]);
234
+ }
235
+ case "feishu_get_thread": {
236
+ const { message_id, count } = args;
237
+ const cmd = [
238
+ "im",
239
+ "+threads-messages-list",
240
+ "--as",
241
+ "user",
242
+ "--thread",
243
+ message_id,
244
+ ];
245
+ if (count) cmd.push("--page-size", String(count));
246
+ return await run(cmd);
247
+ }
248
+ default:
249
+ return error(`未知工具: ${name}`);
250
+ }
251
+ } catch (err) {
252
+ return error(`执行失败: ${err.message}\n${err.stderr || ""}`);
253
+ }
254
+ });
255
+
256
+ async function run(cmdArgs) {
257
+ const { stdout, stderr } = await execFileAsync(LARK_CLI, cmdArgs, {
258
+ timeout: 60000,
259
+ maxBuffer: 1024 * 1024 * 5,
260
+ });
261
+ return { content: [{ type: "text", text: stdout || stderr || "(no output)" }] };
262
+ }
263
+
264
+ function error(msg) {
265
+ return { content: [{ type: "text", text: msg }], isError: true };
266
+ }
267
+
268
+ const transport = new StdioServerTransport();
269
+ await server.connect(transport);