Skip to content

Commit 54d766e

Browse files
committed
feat(main) 0.0.16: 支持user_access_token
1 parent 1d64318 commit 54d766e

14 files changed

+678
-130
lines changed

.env.example

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22
FEISHU_APP_ID=your_feishu_app_id_here
33
FEISHU_APP_SECRET=your_feishu_app_secret_here
44
FEISHU_BASE_URL=https://open.feishu.cn/open-apis
5-
FEISHU_TOKEN_LIFETIME=7200
5+
6+
# 认证凭证类型,支持 tenant(应用级,默认)或 user(用户级,需OAuth授权)注意:只有本地运行服务时支持user凭证,否则就需要配置FEISHU_TOKEN_ENDPOINT,自己实现获取token管理(可以参考 callbackService、feishuAuthService)
7+
FEISHU_AUTH_TYPE=tenant # 可选值:tenant 或 user
8+
9+
# 获取token的接口地址,默认 http://localhost:3333/getToken
10+
# 接口参数:client_id, client_secret, token_type(可选,tenant/user)
11+
# 返回参数:access_token, needAuth, url(需授权时) ,expires_in (单位:s)
12+
FEISHU_TOKEN_ENDPOINT=http://localhost:3333/getToken
13+
14+
615

716
# 服务器配置
817
PORT=3333

FEISHU_CONFIG.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
## 详细步骤
33
### 一、注册飞书应用
44
* url:https://open.feishu.cn/app?lang=zh-CN
5+
56
![注册飞书应用](image/register_application.png)
67
### 二、为应用添加权限
78
创建飞书应用完成后,我们需要为该应用添加飞书文档相关的权限,让应用拥有访问创建文档相关的能力
@@ -14,6 +15,7 @@
1415
{
1516
"scopes": {
1617
"tenant": [
18+
"docx:document.block:convert",
1719
"base:app:read",
1820
"bitable:app",
1921
"bitable:app:readonly",
@@ -37,22 +39,34 @@
3739
"wiki:wiki:readonly"
3840
],
3941
"user": [
42+
"docx:document.block:convert",
43+
"base:app:read",
44+
"bitable:app",
45+
"bitable:app:readonly",
4046
"board:whiteboard:node:read",
47+
"contact:user.employee_id:readonly",
4148
"docs:document.content:read",
4249
"docx:document",
4350
"docx:document:create",
4451
"docx:document:readonly",
4552
"drive:drive",
53+
"drive:drive:readonly",
54+
"drive:file",
4655
"drive:file:upload",
4756
"sheets:spreadsheet",
57+
"sheets:spreadsheet:readonly",
58+
"space:document:retrieve",
59+
"space:folder:create",
60+
"wiki:space:read",
4861
"wiki:space:retrieve",
4962
"wiki:wiki",
50-
"wiki:wiki:readonly"
63+
"wiki:wiki:readonly",
64+
"offline_access"
5165
]
5266
}
5367
}
5468
```
55-
#### 3. 发布审批应用
69+
#### 3. 发布审批应用(注:**可用范围选择全部**
5670
![发布审批应用](image/release.png)
5771
#### 4. 等待管理员审批通过
5872
![发布审批应用完成](image/complete_permissions.png)
@@ -88,12 +102,11 @@
88102

89103
![赋予编辑权限](image/add_edit_permission.png)
90104

91-
### 四、查看应用app Id与app Secret
92-
![应用详情](image/appid.png)
93-
94-
95-
### 五、使用[smithery平台](https://smithery.ai/server/@cso1z/feishu-mcp)快捷接入
105+
### 四、添加redirect_uri回调地址:http://localhost:3333/callback (3333为mcp server默认端口)
106+
![安全设置](image/redirect_uri.png)
96107

108+
### 五、查看应用app Id与app Secret
109+
![应用详情](image/appid.png)
97110

98111
### 六、注
99112
1. 具体可参见[官方云文档常见问题](https://open.feishu.cn/document/server-docs/docs/faq)

README.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
你可以通过以下视频了解 MCP 的实际使用效果和操作流程:
2323

24-
<a href="https://www.bilibili.com/video/BV18z3gzdE1w/?spm_id_from=333.337.search-card.all.click&vd_source=94c14da5a71aeb01f665f159dd3d89c8">
24+
<a href="https://www.bilibili.com/video/BV1z7MdzoEfu/?vd_source=94c14da5a71aeb01f665f159dd3d89c8">
2525
<img src="image/demo.png" alt="飞书 MCP 使用演示" width="800"/>
2626
</a>
2727

@@ -70,7 +70,7 @@
7070
- ~~**优化描述**:7000+ tokens → 3000+ tokens,简化提示,节省请求token~~ 0.0.15 ✅
7171
- ~~**批量增强**:新增批量更新、批量图片上传,单次操作效率提升50%~~ 0.0.15 ✅
7272
- **流程优化**:减少多步调用,实现一键完成复杂任务
73-
- **支持多种凭证类型**:包括 tenant_access_token、app_access_token 和 user_access_token,满足不同场景下的认证需求。
73+
- ~~**支持多种凭证类型**:包括 tenant_access_token和 user_access_token,满足不同场景下的认证需求~~ (飞书应用配置发生变更) 0.0.16 ✅
7474

7575
---
7676

@@ -152,11 +152,17 @@ npx feishu-mcp@latest --feishu-app-id=<你的飞书应用ID> --feishu-app-secret
152152

153153
### 环境变量配置
154154

155-
| 变量名 | 必需 | 描述 | 默认值 |
156-
|--------|------|------|-------|
157-
| `FEISHU_APP_ID` || 飞书应用 ID | - |
158-
| `FEISHU_APP_SECRET` || 飞书应用密钥 | - |
159-
| `PORT` || 服务器端口 | `3333` |
155+
| 变量名 | 必需 | 描述 | 默认值 |
156+
|--------|------|-----------------------------------------------|-------|
157+
| `FEISHU_APP_ID` || 飞书应用 ID | - |
158+
| `FEISHU_APP_SECRET` || 飞书应用密钥 | - |
159+
| `PORT` || 服务器端口 | `3333` |
160+
| `FEISHU_AUTH_TYPE` || 认证凭证类型,建议本地运行时使用 `user`(用户级,需OAuth授权),云端/生产环境使用 `tenant`(应用级,默认) | `tenant` |
161+
| `FEISHU_TOKEN_ENDPOINT` || 获取 token 的接口地址,仅当自定义 token 管理时需要 | `http://localhost:3333/getToken` |
162+
163+
> **注意:**
164+
> - 只有本地运行服务时支持 `user` 凭证,否则需配置 `FEISHU_TOKEN_ENDPOINT`,自行实现 token 获取与管理(可参考 `callbackService``feishuAuthService`)。
165+
> - `FEISHU_TOKEN_ENDPOINT` 接口参数:`client_id`, `client_secret`, `token_type`(可选,tenant/user);返回参数:`access_token`, `needAuth`, `url`(需授权时), `expires_in`(单位:s)。
160166
161167
### 命令行参数
162168

image/redirect_uri.png

50 KB
Loading

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "feishu-mcp",
3-
"version": "0.0.15",
3+
"version": "0.0.16",
44
"description": "Model Context Protocol server for Feishu integration",
55
"type": "module",
66
"main": "dist/index.js",

src/mcp/tools/feishuFolderTools.ts

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,42 +6,49 @@ import {
66
FolderTokenSchema,
77
FolderNameSchema,
88
} from '../../types/feishuSchema.js';
9+
import { Config } from '../../utils/config';
910

1011
/**
1112
* 注册飞书文件夹相关的MCP工具
1213
* @param server MCP服务器实例
1314
* @param feishuService 飞书API服务实例
1415
*/
1516
export function registerFeishuFolderTools(server: McpServer, feishuService: FeishuApiService | null): void {
17+
18+
const config = Config.getInstance();
19+
1620
// 添加获取根文件夹信息工具
17-
// server.tool(
18-
// 'get_feishu_root_folder_info',
19-
// 'Retrieves basic information about the root folder in Feishu Drive. Returns the token, ID and user ID of the root folder, which can be used for subsequent folder operations.',
20-
// {},
21-
// async () => {
22-
// try {
23-
// if (!feishuService) {
24-
// return {
25-
// content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
26-
// };
27-
// }
28-
//
29-
// Logger.info(`开始获取飞书根文件夹信息`);
30-
// const folderInfo = await feishuService.getRootFolderInfo();
31-
// Logger.info(`飞书根文件夹信息获取成功,token: ${folderInfo.token}`);
32-
//
33-
// return {
34-
// content: [{ type: 'text', text: JSON.stringify(folderInfo, null, 2) }],
35-
// };
36-
// } catch (error) {
37-
// Logger.error(`获取飞书根文件夹信息失败:`, error);
38-
// const errorMessage = formatErrorMessage(error, '获取飞书根文件夹信息失败');
39-
// return {
40-
// content: [{ type: 'text', text: errorMessage }],
41-
// };
42-
// }
43-
// },
44-
// );
21+
if (config.feishu.authType === 'user') {
22+
server.tool(
23+
'get_feishu_root_folder_info',
24+
'Retrieves basic information about the root folder in Feishu Drive. Returns the token, ID and user ID of the root folder, which can be used for subsequent folder operations.',
25+
{},
26+
async () => {
27+
try {
28+
if (!feishuService) {
29+
return {
30+
content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
31+
};
32+
}
33+
34+
Logger.info(`开始获取飞书根文件夹信息`);
35+
const folderInfo = await feishuService.getRootFolderInfo();
36+
Logger.info(`飞书根文件夹信息获取成功,token: ${folderInfo.token}`);
37+
38+
return {
39+
content: [{ type: 'text', text: JSON.stringify(folderInfo, null, 2) }],
40+
};
41+
} catch (error) {
42+
Logger.error(`获取飞书根文件夹信息失败:`, error);
43+
const errorMessage = formatErrorMessage(error, '获取飞书根文件夹信息失败');
44+
return {
45+
content: [{ type: 'text', text: errorMessage }],
46+
};
47+
}
48+
},
49+
);
50+
}
51+
4552

4653
// 添加获取文件夹中的文件清单工具
4754
server.tool(

src/server.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
44
import { Logger } from './utils/logger.js';
55
import { SSEConnectionManager } from './manager/sseConnectionManager.js';
66
import { FeishuMcp } from './mcp/feishuMcp.js';
7+
import { callback, getTokenByParams } from './services/callbackService.js';
78

89
export class FeishuMcpServer {
910
private connectionManager: SSEConnectionManager;
@@ -69,6 +70,26 @@ export class FeishuMcpServer {
6970
await transport.handlePostMessage(req, res);
7071
});
7172

73+
app.get('/callback', callback);
74+
75+
app.get('/getToken', async (req: Request, res: Response) => {
76+
const { client_id, client_secret, token_type } = req.query;
77+
if (!client_id || !client_secret) {
78+
res.status(400).json({ code: 400, msg: '缺少 client_id 或 client_secret' });
79+
return;
80+
}
81+
try {
82+
const tokenResult = await getTokenByParams({
83+
client_id: client_id as string,
84+
client_secret: client_secret as string,
85+
token_type: token_type as string
86+
});
87+
res.json({ code: 0, msg: 'success', data: tokenResult });
88+
} catch (e: any) {
89+
res.status(500).json({ code: 500, msg: e.message || '获取token失败' });
90+
}
91+
});
92+
7293
app.listen(port, () => {
7394
Logger.info(`HTTP server listening on port ${port}`);
7495
Logger.info(`SSE endpoint available at http://localhost:${port}/sse`);

src/services/baseService.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,22 +37,6 @@ export abstract class BaseApiService {
3737
*/
3838
protected abstract getBaseUrl(): string;
3939

40-
/**
41-
* 获取API认证端点
42-
* @returns 认证端点URL
43-
*/
44-
protected abstract getAuthEndpoint(): string;
45-
46-
/**
47-
* 检查访问令牌是否过期
48-
* @returns 是否过期
49-
*/
50-
protected isTokenExpired(): boolean {
51-
if (!this.accessToken || !this.tokenExpireTime) return true;
52-
// 预留5分钟的缓冲时间
53-
return Date.now() >= (this.tokenExpireTime - 5 * 60 * 1000);
54-
}
55-
5640
/**
5741
* 获取访问令牌
5842
* @returns 访问令牌

src/services/callbackService.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { Request, Response } from 'express';
2+
import { AuthService } from './feishuAuthService.js';
3+
import { Config } from '../utils/config.js';
4+
import { CacheManager } from '../utils/cache.js';
5+
import { renderFeishuAuthResultHtml } from '../utils/document.js';
6+
7+
// 通用响应码
8+
const CODE = {
9+
SUCCESS: 0,
10+
PARAM_ERROR: 400,
11+
CUSTOM: 500,
12+
};
13+
14+
// 封装响应方法
15+
function sendSuccess(res: Response, data: any) {
16+
const html = renderFeishuAuthResultHtml(data);
17+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
18+
res.status(200).send(html);
19+
}
20+
function sendFail(res: Response, msg: string, code: number = CODE.CUSTOM) {
21+
const html = renderFeishuAuthResultHtml({ error: msg, code });
22+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
23+
res.status(200).send(html);
24+
}
25+
26+
const authService = new AuthService();
27+
const config = Config.getInstance();
28+
29+
export async function callback(req: Request, res: Response) {
30+
const code = req.query.code as string;
31+
const state = req.query.state as string;
32+
console.log(`[callback] query:`, req.query);
33+
if (!code) {
34+
console.log('[callback] 缺少code参数');
35+
return sendFail(res, '缺少code参数', CODE.PARAM_ERROR);
36+
}
37+
// 校验state(clientKey)
38+
const client_id = config.feishu.appId;
39+
const client_secret = config.feishu.appSecret;
40+
const expectedClientKey = await CacheManager.getClientKey(client_id, client_secret);
41+
if (state !== expectedClientKey) {
42+
console.log('[callback] state(clientKey)不匹配');
43+
return sendFail(res, 'state(clientKey)不匹配', CODE.PARAM_ERROR);
44+
}
45+
46+
const redirect_uri = `http://localhost:${config.server.port}/callback`;
47+
const session = (req as any).session;
48+
const code_verifier = session?.code_verifier || undefined;
49+
50+
try {
51+
// 获取 user_access_token
52+
const tokenResp = await authService.getUserTokenByCode({
53+
client_id,
54+
client_secret,
55+
code,
56+
redirect_uri,
57+
code_verifier
58+
});
59+
const data = (tokenResp && typeof tokenResp === 'object') ? tokenResp : undefined;
60+
console.log('[callback] feishu response:', data);
61+
if (!data || data.code !== 0 || !data.access_token) {
62+
return sendFail(res, `获取 access_token 失败,飞书返回: ${JSON.stringify(tokenResp)}`, CODE.CUSTOM);
63+
}
64+
// 获取用户信息
65+
const access_token = data.access_token;
66+
let userInfo = null;
67+
if (access_token) {
68+
userInfo = await authService.getUserInfo(access_token);
69+
console.log('[callback] feishu userInfo:', userInfo);
70+
}
71+
return sendSuccess(res, { ...data, userInfo });
72+
} catch (e) {
73+
console.error('[callback] 请求飞书token或用户信息失败:', e);
74+
return sendFail(res, `请求飞书token或用户信息失败: ${e}`, CODE.CUSTOM);
75+
}
76+
}
77+
78+
export async function getTokenByParams({ client_id, client_secret, token_type }: { client_id: string, client_secret: string, token_type?: string }) {
79+
const authService = new AuthService();
80+
if (client_id) authService.config.feishu.appId = client_id;
81+
if (client_secret) authService.config.feishu.appSecret = client_secret;
82+
if (token_type) authService.config.feishu.authType = token_type === 'user' ? 'user' : 'tenant';
83+
return await authService.getToken();
84+
}

0 commit comments

Comments
 (0)