feishu-user-plugin 1.2.1 → 1.3.1
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 +103 -39
- package/package.json +5 -3
- package/scripts/confirm-version.js +28 -0
- package/scripts/mcp_stdio_bridge.js +97 -0
- package/skills/feishu-user-plugin/references/CLAUDE.md +338 -61
- package/src/cli.js +12 -7
- package/src/config.js +202 -27
- package/src/index.js +429 -55
- package/src/oauth.js +2 -1
- package/src/official.js +313 -1
- package/src/setup.js +19 -3
package/README.md
CHANGED
|
@@ -3,22 +3,25 @@
|
|
|
3
3
|
[](LICENSE)
|
|
4
4
|
[](https://nodejs.org)
|
|
5
5
|
[](https://modelcontextprotocol.io)
|
|
6
|
-
[](#tools)
|
|
7
7
|
[](CONTRIBUTING.md)
|
|
8
8
|
|
|
9
|
-
**All-in-one Feishu/Lark MCP Server --
|
|
9
|
+
**All-in-one Feishu/Lark MCP Server -- 76 tools, 9 skills, 3 auth layers for messaging, docs, bitable, calendar, tasks, drive, and more.**
|
|
10
10
|
|
|
11
|
-
The only MCP server that lets you send messages as your **personal identity** (not a bot), while also integrating the full official Feishu API
|
|
11
|
+
The only MCP server that lets you send messages as your **personal identity** (not a bot), while also integrating the full official Feishu API. Works with Claude Code, Cursor, Windsurf, OpenClaw, and any MCP-compatible client.
|
|
12
12
|
|
|
13
13
|
## Highlights
|
|
14
14
|
|
|
15
15
|
- **Send as yourself** -- Messages show your real name, not a bot. Supports text, rich text, images, files, stickers, and audio.
|
|
16
16
|
- **Read everything** -- Group chats via bot API, P2P (direct messages) via OAuth UAT.
|
|
17
|
-
- **Full Feishu suite** -- Docs, Bitable
|
|
18
|
-
- **3 auth layers** -- Cookie-based user identity, app credentials (Official API), and OAuth UAT (P2P reading).
|
|
17
|
+
- **Full Feishu suite** -- Docs, Bitable, Wiki, Drive, Calendar, Tasks, Contacts -- all in one plugin.
|
|
18
|
+
- **3 auth layers** -- Cookie-based user identity, app credentials (Official API), and OAuth UAT (P2P reading).
|
|
19
|
+
- **Group management** -- Create groups, add/remove members, pin messages, emoji reactions.
|
|
20
|
+
- **Document editing** -- Not just read/create, but insert/update/delete content blocks.
|
|
21
|
+
- **Calendar & Tasks** -- Create events, check free/busy, manage tasks.
|
|
19
22
|
- **9 slash commands** for Claude Code -- `/send`, `/reply`, `/search`, `/digest`, `/doc`, `/table`, `/wiki`, `/drive`, `/status`
|
|
20
23
|
- **Auto session management** -- Cookie heartbeat every 4h, UAT auto-refresh with token rotation.
|
|
21
|
-
- **
|
|
24
|
+
- **Multi-platform** -- Claude Code, Cursor, Windsurf, VS Code, OpenClaw.
|
|
22
25
|
|
|
23
26
|
## Why This Exists
|
|
24
27
|
|
|
@@ -284,6 +287,34 @@ Add to `.vscode/mcp.json` in your project:
|
|
|
284
287
|
}
|
|
285
288
|
```
|
|
286
289
|
|
|
290
|
+
### OpenClaw
|
|
291
|
+
|
|
292
|
+
Add to `~/.openclaw/openclaw.json` (note: key path is `mcp.servers`, not `mcpServers`):
|
|
293
|
+
|
|
294
|
+
```json
|
|
295
|
+
{
|
|
296
|
+
"mcp": {
|
|
297
|
+
"servers": {
|
|
298
|
+
"feishu-user-plugin": {
|
|
299
|
+
"command": "npx",
|
|
300
|
+
"args": ["-y", "feishu-user-plugin"],
|
|
301
|
+
"env": {
|
|
302
|
+
"LARK_COOKIE": "your-cookie-string",
|
|
303
|
+
"LARK_APP_ID": "cli_xxxxxxxxxxxx",
|
|
304
|
+
"LARK_APP_SECRET": "your-app-secret",
|
|
305
|
+
"LARK_USER_ACCESS_TOKEN": "your-uat",
|
|
306
|
+
"LARK_USER_REFRESH_TOKEN": "your-refresh-token"
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Or via CLI: `openclaw mcp set feishu-user-plugin '{"command":"npx","args":["-y","feishu-user-plugin"],"env":{...}}'`
|
|
315
|
+
|
|
316
|
+
> OpenClaw's built-in Feishu channel handles receiving messages (bot identity). This plugin adds user identity messaging + docs/bitable/calendar/tasks.
|
|
317
|
+
|
|
287
318
|
### Windsurf
|
|
288
319
|
|
|
289
320
|
Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
@@ -306,86 +337,119 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
|
|
|
306
337
|
}
|
|
307
338
|
```
|
|
308
339
|
|
|
309
|
-
## Tools (
|
|
340
|
+
## Tools (76 total)
|
|
310
341
|
|
|
311
|
-
### User Identity -- Messaging (cookie auth
|
|
312
|
-
|
|
313
|
-
Send messages as yourself, not as a bot.
|
|
342
|
+
### User Identity -- Messaging (8 tools, cookie auth)
|
|
314
343
|
|
|
315
344
|
| Tool | Description |
|
|
316
345
|
|------|-------------|
|
|
317
346
|
| `send_to_user` | Search user by name + send text -- one step |
|
|
318
347
|
| `send_to_group` | Search group by name + send text -- one step |
|
|
319
|
-
| `send_as_user` | Send text to any chat by ID, supports reply threading
|
|
320
|
-
| `send_image_as_user` | Send image (requires `image_key` from
|
|
321
|
-
| `send_file_as_user` | Send file (requires `file_key` from
|
|
322
|
-
| `send_post_as_user` | Send rich text with title + formatted paragraphs
|
|
348
|
+
| `send_as_user` | Send text to any chat by ID, supports reply threading |
|
|
349
|
+
| `send_image_as_user` | Send image (requires `image_key` from `upload_image`) |
|
|
350
|
+
| `send_file_as_user` | Send file (requires `file_key` from `upload_file`) |
|
|
351
|
+
| `send_post_as_user` | Send rich text with title + formatted paragraphs |
|
|
323
352
|
| `send_sticker_as_user` | Send sticker/emoji |
|
|
324
353
|
| `send_audio_as_user` | Send audio message |
|
|
325
354
|
|
|
326
|
-
### User Identity -- Contacts & Info (cookie auth)
|
|
355
|
+
### User Identity -- Contacts & Info (5 tools, cookie auth)
|
|
327
356
|
|
|
328
357
|
| Tool | Description |
|
|
329
358
|
|------|-------------|
|
|
330
359
|
| `search_contacts` | Search users, bots, or group chats by name |
|
|
331
|
-
| `create_p2p_chat` | Create/get P2P (direct message) chat
|
|
332
|
-
| `get_chat_info` | Group details
|
|
360
|
+
| `create_p2p_chat` | Create/get P2P (direct message) chat |
|
|
361
|
+
| `get_chat_info` | Group details (supports both oc_xxx and numeric ID) |
|
|
333
362
|
| `get_user_info` | User display name lookup by user ID |
|
|
334
363
|
| `get_login_status` | Check cookie, app credentials, and UAT status |
|
|
335
364
|
|
|
336
|
-
### User OAuth UAT -- P2P Chat Reading
|
|
365
|
+
### User OAuth UAT -- P2P Chat Reading (2 tools)
|
|
337
366
|
|
|
338
367
|
| Tool | Description |
|
|
339
368
|
|------|-------------|
|
|
340
|
-
| `read_p2p_messages` | Read P2P (direct message) history
|
|
341
|
-
| `list_user_chats` | List group chats the user is in
|
|
369
|
+
| `read_p2p_messages` | Read P2P (direct message) history |
|
|
370
|
+
| `list_user_chats` | List group chats the user is in |
|
|
342
371
|
|
|
343
|
-
### Official API -- IM (
|
|
372
|
+
### Official API -- IM (17 tools)
|
|
344
373
|
|
|
345
374
|
| Tool | Description |
|
|
346
375
|
|------|-------------|
|
|
347
376
|
| `list_chats` | List all chats the bot has joined |
|
|
348
|
-
| `read_messages` | Read message history (accepts chat name or
|
|
349
|
-
| `
|
|
377
|
+
| `read_messages` | Read message history (accepts chat name, oc_xxx, or numeric ID) |
|
|
378
|
+
| `send_message_as_bot` | Send message as bot to any chat |
|
|
379
|
+
| `reply_message` | Reply to a specific message (as bot) |
|
|
350
380
|
| `forward_message` | Forward a message to another chat |
|
|
351
|
-
|
|
352
|
-
|
|
381
|
+
| `delete_message` | Recall/delete a bot message |
|
|
382
|
+
| `update_message` | Edit a sent bot message |
|
|
383
|
+
| `add_reaction` | Add emoji reaction to a message |
|
|
384
|
+
| `delete_reaction` | Remove emoji reaction |
|
|
385
|
+
| `pin_message` | Pin a message in chat |
|
|
386
|
+
| `unpin_message` | Unpin a message |
|
|
387
|
+
| `create_group` | Create a new group chat |
|
|
388
|
+
| `update_group` | Update group name/description |
|
|
389
|
+
| `list_members` | List group members |
|
|
390
|
+
| `add_members` | Add users to a group |
|
|
391
|
+
| `remove_members` | Remove users from a group |
|
|
392
|
+
| `upload_image` / `upload_file` | Upload image/file, returns key for sending |
|
|
393
|
+
|
|
394
|
+
### Official API -- Documents (7 tools)
|
|
353
395
|
|
|
354
396
|
| Tool | Description |
|
|
355
397
|
|------|-------------|
|
|
356
398
|
| `search_docs` | Search documents by keyword |
|
|
357
|
-
| `read_doc` | Read raw text content
|
|
399
|
+
| `read_doc` | Read raw text content |
|
|
400
|
+
| `get_doc_blocks` | Get structured block tree |
|
|
358
401
|
| `create_doc` | Create a new document |
|
|
402
|
+
| `create_doc_block` | Insert content blocks into a document |
|
|
403
|
+
| `update_doc_block` | Update a specific block |
|
|
404
|
+
| `delete_doc_blocks` | Delete a range of blocks |
|
|
405
|
+
|
|
406
|
+
### Official API -- Bitable (17 tools)
|
|
407
|
+
|
|
408
|
+
| Tool | Description |
|
|
409
|
+
|------|-------------|
|
|
410
|
+
| `create_bitable` | Create a new Bitable app |
|
|
411
|
+
| `list_bitable_tables` / `create_bitable_table` / `delete_bitable_table` | Table management |
|
|
412
|
+
| `list_bitable_fields` / `create_bitable_field` / `update_bitable_field` / `delete_bitable_field` | Field management |
|
|
413
|
+
| `list_bitable_views` | List views |
|
|
414
|
+
| `search_bitable_records` / `get_bitable_record` | Query records |
|
|
415
|
+
| `create_bitable_record` / `update_bitable_record` / `delete_bitable_record` | Single record CRUD |
|
|
416
|
+
| `batch_create_bitable_records` / `batch_update_bitable_records` / `batch_delete_bitable_records` | Batch operations (max 500/call) |
|
|
359
417
|
|
|
360
|
-
### Official API --
|
|
418
|
+
### Official API -- Calendar (5 tools)
|
|
361
419
|
|
|
362
420
|
| Tool | Description |
|
|
363
421
|
|------|-------------|
|
|
364
|
-
| `
|
|
365
|
-
| `
|
|
366
|
-
| `
|
|
367
|
-
| `
|
|
368
|
-
| `
|
|
422
|
+
| `list_calendars` | List accessible calendars |
|
|
423
|
+
| `create_calendar_event` | Create a calendar event |
|
|
424
|
+
| `list_calendar_events` | List events in a calendar |
|
|
425
|
+
| `delete_calendar_event` | Delete an event |
|
|
426
|
+
| `get_freebusy` | Check user availability |
|
|
369
427
|
|
|
370
|
-
### Official API --
|
|
428
|
+
### Official API -- Tasks (5 tools)
|
|
371
429
|
|
|
372
430
|
| Tool | Description |
|
|
373
431
|
|------|-------------|
|
|
374
|
-
| `
|
|
375
|
-
| `
|
|
376
|
-
| `
|
|
432
|
+
| `create_task` | Create a task |
|
|
433
|
+
| `get_task` | Get task details |
|
|
434
|
+
| `list_tasks` | List tasks |
|
|
435
|
+
| `update_task` | Update a task |
|
|
436
|
+
| `complete_task` | Mark task as complete |
|
|
377
437
|
|
|
378
|
-
### Official API -- Drive
|
|
438
|
+
### Official API -- Drive (5 tools)
|
|
379
439
|
|
|
380
440
|
| Tool | Description |
|
|
381
441
|
|------|-------------|
|
|
382
442
|
| `list_files` | List files in a folder |
|
|
383
443
|
| `create_folder` | Create a new folder |
|
|
444
|
+
| `copy_file` | Copy a file |
|
|
445
|
+
| `move_file` | Move a file |
|
|
446
|
+
| `delete_file` | Delete a file/folder |
|
|
384
447
|
|
|
385
|
-
### Official API -- Contacts
|
|
448
|
+
### Official API -- Wiki & Contacts (4 tools)
|
|
386
449
|
|
|
387
450
|
| Tool | Description |
|
|
388
451
|
|------|-------------|
|
|
452
|
+
| `list_wiki_spaces` / `search_wiki` / `list_wiki_nodes` | Wiki spaces, search, browse |
|
|
389
453
|
| `find_user` | Find user by email or mobile number |
|
|
390
454
|
|
|
391
455
|
## Claude Code Slash Commands (9 skills)
|
|
@@ -444,7 +508,7 @@ feishu-user-plugin/
|
|
|
444
508
|
│ ├── SKILL.md # Main skill definition (trigger, tools, auth)
|
|
445
509
|
│ └── references/ # 8 skill reference docs + CLAUDE.md
|
|
446
510
|
├── src/
|
|
447
|
-
│ ├── index.js # MCP server entry point (
|
|
511
|
+
│ ├── index.js # MCP server entry point (76 tools)
|
|
448
512
|
│ ├── client.js # User identity client (Protobuf gateway)
|
|
449
513
|
│ ├── official.js # Official API client (REST, UAT)
|
|
450
514
|
│ ├── utils.js # ID generators, cookie parser
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "feishu-user-plugin",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "All-in-one Feishu plugin for Claude Code
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"description": "All-in-one Feishu plugin for Claude Code & Codex — messaging, docs, bitable, wiki, drive. 66 tools + 9 skills, 3 auth layers.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"feishu-user-plugin": "src/cli.js"
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"start": "node src/index.js",
|
|
11
11
|
"test": "node src/test-all.js",
|
|
12
12
|
"test:quick": "node src/test-send.js",
|
|
13
|
-
"oauth": "node src/oauth.js"
|
|
13
|
+
"oauth": "node src/oauth.js",
|
|
14
|
+
"prepublishOnly": "node scripts/confirm-version.js"
|
|
14
15
|
},
|
|
15
16
|
"keywords": [
|
|
16
17
|
"feishu",
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
"files": [
|
|
38
39
|
"src/",
|
|
39
40
|
"proto/",
|
|
41
|
+
"scripts/",
|
|
40
42
|
".claude-plugin/",
|
|
41
43
|
"skills/",
|
|
42
44
|
".mcp.json.example",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Pre-publish version confirmation gate.
|
|
4
|
+
* Runs as prepublishOnly — blocks `npm publish` until version is confirmed.
|
|
5
|
+
* Automatically skipped in CI (GitHub Actions handles tag/version check separately).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
if (process.env.CI || process.env.GITHUB_ACTIONS) {
|
|
9
|
+
process.exit(0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const readline = require('readline');
|
|
13
|
+
const pkg = require('../package.json');
|
|
14
|
+
|
|
15
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
16
|
+
|
|
17
|
+
console.log(`\n Package: ${pkg.name}`);
|
|
18
|
+
console.log(` Version: ${pkg.version}`);
|
|
19
|
+
console.log(` Tools: ${pkg.description}\n`);
|
|
20
|
+
|
|
21
|
+
rl.question(` Confirm publish v${pkg.version}? (y/N): `, (answer) => {
|
|
22
|
+
rl.close();
|
|
23
|
+
if (answer.trim().toLowerCase() !== 'y') {
|
|
24
|
+
console.error('\n Publish cancelled. Update version in package.json if needed.\n');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
console.log(' Confirmed. Proceeding with publish...\n');
|
|
28
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const defaultCommand = ['node', '/Users/abble/feishu-user-plugin/src/index.js'];
|
|
6
|
+
const childCommand = process.argv.slice(2);
|
|
7
|
+
const [command, ...args] = childCommand.length > 0 ? childCommand : defaultCommand;
|
|
8
|
+
|
|
9
|
+
const child = spawn(command, args, {
|
|
10
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
11
|
+
env: process.env,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
let parentMode = null; // 'content-length' | 'newline'
|
|
15
|
+
let parentInBuffer = Buffer.alloc(0);
|
|
16
|
+
let childOutBuffer = '';
|
|
17
|
+
|
|
18
|
+
function writeToParent(jsonLine) {
|
|
19
|
+
if (parentMode === 'content-length') {
|
|
20
|
+
const body = Buffer.from(jsonLine, 'utf8');
|
|
21
|
+
process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
|
|
22
|
+
process.stdout.write(body);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
process.stdout.write(jsonLine + '\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function handleParentMessage(message) {
|
|
29
|
+
const line = typeof message === 'string' ? message : JSON.stringify(message);
|
|
30
|
+
child.stdin.write(line + '\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function tryParseParentBuffer() {
|
|
34
|
+
while (parentInBuffer.length > 0) {
|
|
35
|
+
if (parentMode === 'content-length' || (!parentMode && parentInBuffer.includes(Buffer.from('\r\n\r\n')))) {
|
|
36
|
+
parentMode = 'content-length';
|
|
37
|
+
const headerEnd = parentInBuffer.indexOf(Buffer.from('\r\n\r\n'));
|
|
38
|
+
if (headerEnd === -1) return;
|
|
39
|
+
const headerText = parentInBuffer.subarray(0, headerEnd).toString('utf8');
|
|
40
|
+
const match = headerText.match(/Content-Length:\s*(\d+)/i);
|
|
41
|
+
if (!match) {
|
|
42
|
+
process.stderr.write('[mcp-bridge] Missing Content-Length header\n');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
const bodyLength = Number(match[1]);
|
|
46
|
+
const totalLength = headerEnd + 4 + bodyLength;
|
|
47
|
+
if (parentInBuffer.length < totalLength) return;
|
|
48
|
+
const body = parentInBuffer.subarray(headerEnd + 4, totalLength).toString('utf8');
|
|
49
|
+
parentInBuffer = parentInBuffer.subarray(totalLength);
|
|
50
|
+
handleParentMessage(body);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
parentMode = 'newline';
|
|
55
|
+
const newlineIndex = parentInBuffer.indexOf(0x0a);
|
|
56
|
+
if (newlineIndex === -1) return;
|
|
57
|
+
const line = parentInBuffer.subarray(0, newlineIndex).toString('utf8').replace(/\r$/, '');
|
|
58
|
+
parentInBuffer = parentInBuffer.subarray(newlineIndex + 1);
|
|
59
|
+
if (line.trim()) handleParentMessage(line);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
process.stdin.on('data', (chunk) => {
|
|
64
|
+
parentInBuffer = Buffer.concat([parentInBuffer, chunk]);
|
|
65
|
+
tryParseParentBuffer();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
child.stdout.on('data', (chunk) => {
|
|
69
|
+
childOutBuffer += chunk.toString('utf8');
|
|
70
|
+
while (true) {
|
|
71
|
+
const newlineIndex = childOutBuffer.indexOf('\n');
|
|
72
|
+
if (newlineIndex === -1) break;
|
|
73
|
+
const line = childOutBuffer.slice(0, newlineIndex).replace(/\r$/, '');
|
|
74
|
+
childOutBuffer = childOutBuffer.slice(newlineIndex + 1);
|
|
75
|
+
if (line.trim()) writeToParent(line);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
child.stderr.on('data', (chunk) => {
|
|
80
|
+
process.stderr.write(chunk);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
child.on('exit', (code, signal) => {
|
|
84
|
+
if (signal) {
|
|
85
|
+
process.stderr.write(`[mcp-bridge] Child exited via signal ${signal}\n`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
process.exit(code ?? 0);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
child.on('error', (error) => {
|
|
92
|
+
process.stderr.write(`[mcp-bridge] Failed to spawn child: ${error.message}\n`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
process.on('SIGINT', () => child.kill('SIGINT'));
|
|
97
|
+
process.on('SIGTERM', () => child.kill('SIGTERM'));
|