@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 +21 -0
- package/README.md +135 -0
- package/bin/cli.mjs +158 -0
- package/package.json +43 -0
- package/server.mjs +269 -0
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);
|