feishu-mcp 0.1.6 → 0.1.8
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 +73 -33
- package/dist/mcp/feishuMcp.js +1 -1
- package/dist/mcp/tools/feishuBlockTools.js +163 -35
- package/dist/mcp/tools/feishuFolderTools.js +95 -30
- package/dist/mcp/tools/feishuTools.js +84 -30
- package/dist/services/baseService.js +13 -2
- package/dist/services/blockFactory.js +17 -0
- package/dist/services/feishuApiService.js +738 -65
- package/dist/types/feishuSchema.js +60 -5
- package/dist/utils/auth/tokenCacheManager.js +112 -0
- package/dist/utils/error.js +24 -0
- package/package.json +1 -1
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
// import { z } from 'zod';
|
|
2
1
|
import { formatErrorMessage } from '../../utils/error.js';
|
|
3
2
|
import { Logger } from '../../utils/logger.js';
|
|
4
|
-
import { DocumentIdSchema,
|
|
3
|
+
import { DocumentIdSchema, DocumentIdOrWikiIdSchema, DocumentTypeSchema,
|
|
5
4
|
// BlockIdSchema,
|
|
6
|
-
SearchKeySchema, WhiteboardIdSchema, DocumentTitleSchema,
|
|
5
|
+
SearchKeySchema, SearchTypeSchema, PageTokenSchema, OffsetSchema, WhiteboardIdSchema, DocumentTitleSchema, FolderTokenOptionalSchema, WikiSpaceNodeContextSchema, } from '../../types/feishuSchema.js';
|
|
7
6
|
/**
|
|
8
7
|
* 注册飞书相关的MCP工具
|
|
9
8
|
* @param server MCP服务器实例
|
|
@@ -11,42 +10,94 @@ SearchKeySchema, WhiteboardIdSchema, DocumentTitleSchema, FolderTokenSchema, } f
|
|
|
11
10
|
*/
|
|
12
11
|
export function registerFeishuTools(server, feishuService) {
|
|
13
12
|
// 添加创建飞书文档工具
|
|
14
|
-
server.tool('create_feishu_document', 'Creates a new Feishu document and returns its information.
|
|
13
|
+
server.tool('create_feishu_document', 'Creates a new Feishu document and returns its information. Supports two modes: (1) Feishu Drive folder mode: use folderToken to create a document in a folder. (2) Wiki space node mode: use wikiContext with spaceId (and optional parentNodeToken) to create a node (document) in a wiki space. IMPORTANT: In wiki spaces, documents are nodes themselves - they can act as parent nodes containing child documents, and can also be edited as regular documents. The created node returns both node_token (node ID, can be used as parentNodeToken for creating child nodes) and obj_token (document ID, can be used for document editing operations like get_feishu_document_blocks, batch_create_feishu_blocks, etc.). Only one mode can be used at a time - provide either folderToken OR wikiContext, not both.', {
|
|
15
14
|
title: DocumentTitleSchema,
|
|
16
|
-
folderToken:
|
|
17
|
-
|
|
15
|
+
folderToken: FolderTokenOptionalSchema,
|
|
16
|
+
wikiContext: WikiSpaceNodeContextSchema,
|
|
17
|
+
}, async ({ title, folderToken, wikiContext }) => {
|
|
18
18
|
try {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
if (!feishuService) {
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// 参数验证:必须提供 folderToken 或 wikiContext 之一,但不能同时提供
|
|
25
|
+
if (folderToken && wikiContext) {
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: 'text', text: '错误:不能同时提供 folderToken 和 wikiContext 参数,请选择其中一种模式。\n- 使用 folderToken 在飞书文档目录中创建文档\n- 使用 wikiContext 在知识库中创建节点(文档)' }],
|
|
28
|
+
};
|
|
23
29
|
}
|
|
24
|
-
|
|
30
|
+
if (!folderToken && !wikiContext) {
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: 'text', text: '错误:必须提供 folderToken(飞书文档目录模式)或 wikiContext(知识库节点模式)参数之一。' }],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// 模式一:飞书文档目录模式
|
|
36
|
+
if (folderToken) {
|
|
37
|
+
Logger.info(`开始创建飞书文档(文件夹模式),标题: ${title},文件夹Token: ${folderToken}`);
|
|
38
|
+
const newDoc = await feishuService.createDocument(title, folderToken);
|
|
39
|
+
if (!newDoc) {
|
|
40
|
+
throw new Error('创建文档失败,未返回文档信息');
|
|
41
|
+
}
|
|
42
|
+
Logger.info(`飞书文档创建成功,文档ID: ${newDoc.objToken || newDoc.document_id}`);
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: 'text', text: JSON.stringify(newDoc, null, 2) }],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// 模式二:知识库节点模式
|
|
48
|
+
if (wikiContext) {
|
|
49
|
+
const { spaceId, parentNodeToken } = wikiContext;
|
|
50
|
+
if (!spaceId) {
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: 'text', text: '错误:使用 wikiContext 模式时,必须提供 spaceId。' }],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
Logger.info(`开始创建知识库节点,标题: ${title},知识空间ID: ${spaceId},父节点Token: ${parentNodeToken || 'null(根节点)'}`);
|
|
56
|
+
const node = await feishuService.createWikiSpaceNode(spaceId, title, parentNodeToken);
|
|
57
|
+
if (!node) {
|
|
58
|
+
throw new Error('创建知识库节点失败,未返回节点信息');
|
|
59
|
+
}
|
|
60
|
+
// 构建返回信息,说明知识库节点的特殊性质
|
|
61
|
+
const result = {
|
|
62
|
+
...node,
|
|
63
|
+
_note: '知识库节点既是节点又是文档:node_token 可作为父节点使用,obj_token 可用于文档编辑操作'
|
|
64
|
+
};
|
|
65
|
+
Logger.info(`知识库节点创建成功,node_token: ${node.node_token}, obj_token: ${node.obj_token}`);
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// 理论上不会到达这里
|
|
25
71
|
return {
|
|
26
|
-
content: [{ type: 'text', text:
|
|
72
|
+
content: [{ type: 'text', text: '错误:未知错误' }],
|
|
27
73
|
};
|
|
28
74
|
}
|
|
29
75
|
catch (error) {
|
|
30
|
-
Logger.error(
|
|
76
|
+
Logger.error(`创建文档失败:`, error);
|
|
31
77
|
const errorMessage = formatErrorMessage(error);
|
|
32
78
|
return {
|
|
33
|
-
content: [{ type: 'text', text:
|
|
79
|
+
content: [{ type: 'text', text: `创建文档失败: ${errorMessage}` }],
|
|
34
80
|
};
|
|
35
81
|
}
|
|
36
82
|
});
|
|
37
|
-
//
|
|
38
|
-
server.tool('get_feishu_document_info', 'Retrieves basic information about a Feishu document. Use this to verify a document exists, check access permissions, or get metadata like title, type, and creation information.', {
|
|
39
|
-
documentId:
|
|
40
|
-
|
|
83
|
+
// 添加获取飞书文档信息工具(支持普通文档和Wiki文档)
|
|
84
|
+
server.tool('get_feishu_document_info', 'Retrieves basic information about a Feishu document or Wiki node. Supports both regular documents (via document ID/URL) and Wiki documents (via Wiki URL/token). Use this to verify a document exists, check access permissions, or get metadata like title, type, and creation information. For Wiki documents, returns complete node information including documentId (obj_token) for document editing operations, and space_id and node_token for creating child nodes. ', {
|
|
85
|
+
documentId: DocumentIdOrWikiIdSchema,
|
|
86
|
+
documentType: DocumentTypeSchema,
|
|
87
|
+
}, async ({ documentId, documentType }) => {
|
|
41
88
|
try {
|
|
42
89
|
if (!feishuService) {
|
|
43
90
|
return {
|
|
44
91
|
content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
|
|
45
92
|
};
|
|
46
93
|
}
|
|
47
|
-
Logger.info(`开始获取飞书文档信息,文档ID: ${documentId}`);
|
|
48
|
-
const docInfo = await feishuService.getDocumentInfo(documentId);
|
|
49
|
-
|
|
94
|
+
Logger.info(`开始获取飞书文档信息,文档ID: ${documentId}, 类型: ${documentType || 'auto'}`);
|
|
95
|
+
const docInfo = await feishuService.getDocumentInfo(documentId, documentType);
|
|
96
|
+
if (!docInfo) {
|
|
97
|
+
throw new Error('获取文档信息失败,未返回数据');
|
|
98
|
+
}
|
|
99
|
+
const title = docInfo.title || docInfo.document?.title || '未知标题';
|
|
100
|
+
Logger.info(`飞书文档信息获取成功,标题: ${title}, 类型: ${docInfo._type || 'document'}`);
|
|
50
101
|
return {
|
|
51
102
|
content: [{ type: 'text', text: JSON.stringify(docInfo, null, 2) }],
|
|
52
103
|
};
|
|
@@ -92,7 +143,7 @@ export function registerFeishuTools(server, feishuService) {
|
|
|
92
143
|
// },
|
|
93
144
|
// );
|
|
94
145
|
// 添加获取飞书文档块工具
|
|
95
|
-
server.tool('get_feishu_document_blocks', 'Retrieves the block structure information of a Feishu document. Essential to use before inserting content to understand document structure and determine correct insertion positions. Returns a detailed hierarchy of blocks with their IDs, types, and content. Note: For Feishu wiki links (https://xxx.feishu.cn/wiki/xxx)
|
|
146
|
+
server.tool('get_feishu_document_blocks', 'Retrieves the block structure information of a Feishu document. Essential to use before inserting content to understand document structure and determine correct insertion positions. Returns a detailed hierarchy of blocks with their IDs, types, and content. Note: For Feishu wiki links (https://xxx.feishu.cn/wiki/xxx), use get_feishu_document_info to get document information, then use the returned documentId for editing operations.', {
|
|
96
147
|
documentId: DocumentIdSchema,
|
|
97
148
|
}, async ({ documentId }) => {
|
|
98
149
|
try {
|
|
@@ -183,19 +234,22 @@ export function registerFeishuTools(server, feishuService) {
|
|
|
183
234
|
// }
|
|
184
235
|
// },
|
|
185
236
|
// );
|
|
186
|
-
//
|
|
187
|
-
server.tool('search_feishu_documents', 'Searches for documents in Feishu. Supports keyword-based search
|
|
237
|
+
// 添加搜索文档工具(支持文档和知识库搜索)
|
|
238
|
+
server.tool('search_feishu_documents', 'Searches for documents and/or Wiki knowledge base nodes in Feishu. Supports keyword-based search with type filtering (document, wiki, or both). Returns document and wiki information including title, type, and owner. Supports pagination: use offset for document search pagination and pageToken for wiki search pagination. Each type (document or wiki) can return up to 100 results maximum per search. Default page size is 20 items.', {
|
|
188
239
|
searchKey: SearchKeySchema,
|
|
189
|
-
|
|
240
|
+
searchType: SearchTypeSchema,
|
|
241
|
+
offset: OffsetSchema,
|
|
242
|
+
pageToken: PageTokenSchema,
|
|
243
|
+
}, async ({ searchKey, searchType, offset, pageToken }) => {
|
|
190
244
|
try {
|
|
191
245
|
if (!feishuService) {
|
|
192
246
|
return {
|
|
193
247
|
content: [{ type: 'text', text: 'Feishu service is not initialized. Please check the configuration.' }],
|
|
194
248
|
};
|
|
195
249
|
}
|
|
196
|
-
Logger.info(
|
|
197
|
-
const searchResult = await feishuService.
|
|
198
|
-
Logger.info(
|
|
250
|
+
Logger.info(`开始搜索,关键字: ${searchKey}, 类型: ${searchType || 'both'}, offset: ${offset || 0}, pageToken: ${pageToken || '无'}`);
|
|
251
|
+
const searchResult = await feishuService.search(searchKey, searchType || 'both', offset, pageToken);
|
|
252
|
+
Logger.info(`搜索完成,文档: ${searchResult.documents?.length || 0} 条,知识库: ${searchResult.wikis?.length || 0} 条`);
|
|
199
253
|
return {
|
|
200
254
|
content: [
|
|
201
255
|
{ type: 'text', text: JSON.stringify(searchResult, null, 2) },
|
|
@@ -203,11 +257,11 @@ export function registerFeishuTools(server, feishuService) {
|
|
|
203
257
|
};
|
|
204
258
|
}
|
|
205
259
|
catch (error) {
|
|
206
|
-
Logger.error(
|
|
260
|
+
Logger.error(`搜索失败:`, error);
|
|
207
261
|
const errorMessage = formatErrorMessage(error);
|
|
208
262
|
return {
|
|
209
263
|
content: [
|
|
210
|
-
{ type: 'text', text:
|
|
264
|
+
{ type: 'text', text: `搜索失败: ${errorMessage}` },
|
|
211
265
|
],
|
|
212
266
|
};
|
|
213
267
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import axios, { AxiosError } from 'axios';
|
|
2
2
|
import FormData from 'form-data';
|
|
3
3
|
import { Logger } from '../utils/logger.js';
|
|
4
|
-
import { formatErrorMessage, AuthRequiredError } from '../utils/error.js';
|
|
4
|
+
import { formatErrorMessage, AuthRequiredError, ScopeInsufficientError } from '../utils/error.js';
|
|
5
5
|
import { Config } from '../utils/config.js';
|
|
6
6
|
import { TokenCacheManager, UserContextManager, AuthUtils } from '../utils/auth/index.js';
|
|
7
7
|
/**
|
|
@@ -126,6 +126,10 @@ export class BaseApiService {
|
|
|
126
126
|
}
|
|
127
127
|
catch (error) {
|
|
128
128
|
const config = Config.getInstance().feishu;
|
|
129
|
+
// 优先处理权限不足异常
|
|
130
|
+
if (error instanceof ScopeInsufficientError) {
|
|
131
|
+
return this.handleScopeInsufficientError(error);
|
|
132
|
+
}
|
|
129
133
|
// 处理授权异常
|
|
130
134
|
if (error instanceof AuthRequiredError) {
|
|
131
135
|
return this.handleAuthFailure(config.authType === "tenant", clientKey, baseUrl, userKey);
|
|
@@ -212,6 +216,13 @@ export class BaseApiService {
|
|
|
212
216
|
async delete(endpoint, data, needsAuth = true) {
|
|
213
217
|
return this.request(endpoint, 'DELETE', data, needsAuth);
|
|
214
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* 处理权限不足异常
|
|
221
|
+
* @param error 权限不足错误
|
|
222
|
+
*/
|
|
223
|
+
handleScopeInsufficientError(error) {
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
215
226
|
/**
|
|
216
227
|
* 处理认证失败
|
|
217
228
|
* @param tenant 是否是tenant
|
|
@@ -300,7 +311,7 @@ export class BaseApiService {
|
|
|
300
311
|
const { appId, appSecret } = Config.getInstance().feishu;
|
|
301
312
|
const clientKey = AuthUtils.generateClientKey(userKey);
|
|
302
313
|
const redirect_uri = `${baseUrl}/callback`;
|
|
303
|
-
const scope = encodeURIComponent('base:app:read bitable:app bitable:app:readonly board:whiteboard:node:read contact:user.employee_id:readonly docs:document.content:read docx:document docx:document.block:convert docx:document:create docx:document:readonly drive:drive drive:drive:readonly drive:file drive:file:upload sheets:spreadsheet sheets:spreadsheet:readonly space:document:retrieve space:folder:create wiki:space:read wiki:space:retrieve wiki:wiki wiki:wiki:readonly offline_access');
|
|
314
|
+
const scope = encodeURIComponent('base:app:read bitable:app bitable:app:readonly board:whiteboard:node:read board:whiteboard:node:create contact:user.employee_id:readonly docs:document.content:read docx:document docx:document.block:convert docx:document:create docx:document:readonly drive:drive drive:drive:readonly drive:file drive:file:upload sheets:spreadsheet sheets:spreadsheet:readonly space:document:retrieve space:folder:create wiki:space:read wiki:space:retrieve wiki:wiki wiki:wiki:readonly offline_access');
|
|
304
315
|
const state = AuthUtils.encodeState(appId, appSecret, clientKey, redirect_uri);
|
|
305
316
|
return `https://accounts.feishu.cn/open-apis/authen/v1/authorize?client_id=${appId}&redirect_uri=${encodeURIComponent(redirect_uri)}&scope=${scope}&state=${state}`;
|
|
306
317
|
}
|
|
@@ -10,6 +10,7 @@ export var BlockType;
|
|
|
10
10
|
BlockType["LIST"] = "list";
|
|
11
11
|
BlockType["IMAGE"] = "image";
|
|
12
12
|
BlockType["MERMAID"] = "mermaid";
|
|
13
|
+
BlockType["WHITEBOARD"] = "whiteboard";
|
|
13
14
|
})(BlockType || (BlockType = {}));
|
|
14
15
|
/**
|
|
15
16
|
* 对齐方式枚举
|
|
@@ -78,6 +79,8 @@ export class BlockFactory {
|
|
|
78
79
|
return this.createImageBlock(options);
|
|
79
80
|
case BlockType.MERMAID:
|
|
80
81
|
return this.createMermaidBlock(options);
|
|
82
|
+
case BlockType.WHITEBOARD:
|
|
83
|
+
return this.createWhiteboardBlock(options);
|
|
81
84
|
default:
|
|
82
85
|
Logger.error(`不支持的块类型: ${type}`);
|
|
83
86
|
throw new Error(`不支持的块类型: ${type}`);
|
|
@@ -246,6 +249,20 @@ export class BlockFactory {
|
|
|
246
249
|
},
|
|
247
250
|
};
|
|
248
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* 创建画板块内容(空画板块,需要后续填充内容)
|
|
254
|
+
* @param options 画板块选项
|
|
255
|
+
* @returns 画板块内容对象
|
|
256
|
+
*/
|
|
257
|
+
createWhiteboardBlock(options = {}) {
|
|
258
|
+
const { align = AlignType.CENTER } = options;
|
|
259
|
+
return {
|
|
260
|
+
block_type: 43, // 43表示画板块
|
|
261
|
+
board: {
|
|
262
|
+
align: align // 1 居左,2 居中,3 居右
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
249
266
|
/**
|
|
250
267
|
* 创建表格块
|
|
251
268
|
* @param options 表格块选项
|