feishu-mcp 0.0.5 → 0.0.7
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 +1 -6
- package/dist/config.js +22 -100
- package/dist/index.js +14 -13
- package/dist/server.js +193 -370
- package/dist/services/baseService.js +204 -0
- package/dist/services/blockFactory.js +184 -0
- package/dist/services/feishu.js +16 -148
- package/dist/services/feishuApiService.js +488 -0
- package/dist/services/feishuBlockService.js +179 -0
- package/dist/services/feishuService.js +475 -0
- package/dist/types/feishuSchema.js +97 -0
- package/dist/utils/cache.js +221 -0
- package/dist/utils/config.js +363 -0
- package/dist/utils/document.js +74 -0
- package/dist/utils/error.js +154 -0
- package/dist/utils/logger.js +257 -0
- package/dist/utils/paramUtils.js +193 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1,138 +1,13 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { FeishuService } from './services/feishu.js';
|
|
4
3
|
import express from 'express';
|
|
5
4
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
error: (...args) => {
|
|
11
|
-
console.error(...args);
|
|
12
|
-
},
|
|
13
|
-
};
|
|
14
|
-
// 添加一个工具类方法,用于格式化错误信息
|
|
15
|
-
function formatErrorMessage(error) {
|
|
16
|
-
if (error instanceof Error) {
|
|
17
|
-
return error.message;
|
|
18
|
-
}
|
|
19
|
-
else if (typeof error === 'string') {
|
|
20
|
-
return error;
|
|
21
|
-
}
|
|
22
|
-
else if (error && typeof error === 'object') {
|
|
23
|
-
try {
|
|
24
|
-
// 处理包含apiError字段的FeishuError对象
|
|
25
|
-
if (error.apiError) {
|
|
26
|
-
const apiError = error.apiError;
|
|
27
|
-
let errorMsg = '';
|
|
28
|
-
// 处理标准飞书API错误格式
|
|
29
|
-
if (apiError.code && apiError.msg) {
|
|
30
|
-
errorMsg = `${apiError.msg} (错误码: ${apiError.code})`;
|
|
31
|
-
// 添加字段验证错误信息
|
|
32
|
-
if (apiError.error && apiError.error.field_violations && apiError.error.field_violations.length > 0) {
|
|
33
|
-
const violations = apiError.error.field_violations;
|
|
34
|
-
errorMsg += '\n字段验证错误:';
|
|
35
|
-
violations.forEach((violation) => {
|
|
36
|
-
let detail = `\n - ${violation.field}`;
|
|
37
|
-
if (violation.description) {
|
|
38
|
-
detail += `: ${violation.description}`;
|
|
39
|
-
}
|
|
40
|
-
if (violation.value) {
|
|
41
|
-
detail += `,提供的值: ${violation.value}`;
|
|
42
|
-
}
|
|
43
|
-
errorMsg += detail;
|
|
44
|
-
});
|
|
45
|
-
// 添加排查建议链接
|
|
46
|
-
if (apiError.error.troubleshooter) {
|
|
47
|
-
errorMsg += `\n\n${apiError.error.troubleshooter}`;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return errorMsg;
|
|
51
|
-
}
|
|
52
|
-
// 如果apiError没有标准结构,尝试序列化
|
|
53
|
-
return `API错误: ${JSON.stringify(apiError)}`;
|
|
54
|
-
}
|
|
55
|
-
// 处理飞书API特定的错误格式
|
|
56
|
-
if (error.code && error.msg) {
|
|
57
|
-
// 基本错误信息
|
|
58
|
-
let errorMsg = `${error.msg} (错误码: ${error.code})`;
|
|
59
|
-
// 如果有详细的验证错误信息
|
|
60
|
-
if (error.error && error.error.field_violations && error.error.field_violations.length > 0) {
|
|
61
|
-
const violations = error.error.field_violations;
|
|
62
|
-
errorMsg += '\n字段验证错误:';
|
|
63
|
-
violations.forEach((violation) => {
|
|
64
|
-
let detail = `\n - ${violation.field}`;
|
|
65
|
-
if (violation.description) {
|
|
66
|
-
detail += `: ${violation.description}`;
|
|
67
|
-
}
|
|
68
|
-
if (violation.value) {
|
|
69
|
-
detail += `,提供的值: ${violation.value}`;
|
|
70
|
-
}
|
|
71
|
-
errorMsg += detail;
|
|
72
|
-
});
|
|
73
|
-
// 添加排查建议链接(如果有)
|
|
74
|
-
if (error.error.troubleshooter) {
|
|
75
|
-
errorMsg += `\n\n${error.error.troubleshooter}`;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return errorMsg;
|
|
79
|
-
}
|
|
80
|
-
// 处理 {status, err} 格式的错误
|
|
81
|
-
if (error.status && error.err) {
|
|
82
|
-
return `操作失败 (状态码: ${error.status}): ${error.err}`;
|
|
83
|
-
}
|
|
84
|
-
// 尝试提取API错误信息,通常在错误对象的message或error字段中
|
|
85
|
-
if (error.message) {
|
|
86
|
-
return error.message;
|
|
87
|
-
}
|
|
88
|
-
else if (error.error) {
|
|
89
|
-
if (typeof error.error === 'string') {
|
|
90
|
-
return error.error;
|
|
91
|
-
}
|
|
92
|
-
else if (error.error.message) {
|
|
93
|
-
return error.error.message;
|
|
94
|
-
}
|
|
95
|
-
else if (error.error.field_violations) {
|
|
96
|
-
// 处理错误嵌套在error对象中的情况
|
|
97
|
-
const violations = error.error.field_violations;
|
|
98
|
-
let errorMsg = '字段验证错误:';
|
|
99
|
-
violations.forEach((violation) => {
|
|
100
|
-
let detail = `\n - ${violation.field}`;
|
|
101
|
-
if (violation.description) {
|
|
102
|
-
detail += `: ${violation.description}`;
|
|
103
|
-
}
|
|
104
|
-
if (violation.value) {
|
|
105
|
-
detail += `,提供的值: ${violation.value}`;
|
|
106
|
-
}
|
|
107
|
-
errorMsg += detail;
|
|
108
|
-
});
|
|
109
|
-
return errorMsg;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
else if (error.code || error.status) {
|
|
113
|
-
// 处理HTTP错误或API错误码
|
|
114
|
-
const code = error.code || error.status;
|
|
115
|
-
const msg = error.statusText || error.msg || 'Unknown error';
|
|
116
|
-
return `操作失败 (错误码: ${code}): ${msg}`;
|
|
117
|
-
}
|
|
118
|
-
// 如果上述都不符合,尝试将整个对象序列化(但移除敏感信息)
|
|
119
|
-
const safeError = { ...error };
|
|
120
|
-
// 移除可能的敏感信息
|
|
121
|
-
['token', 'secret', 'password', 'key', 'credentials'].forEach(key => {
|
|
122
|
-
if (key in safeError)
|
|
123
|
-
delete safeError[key];
|
|
124
|
-
});
|
|
125
|
-
return `发生错误: ${JSON.stringify(safeError)}`;
|
|
126
|
-
}
|
|
127
|
-
catch (e) {
|
|
128
|
-
console.error("Error formatting error message:", e);
|
|
129
|
-
return '发生未知错误';
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return '发生未知错误';
|
|
133
|
-
}
|
|
5
|
+
import { formatErrorMessage } from './utils/error.js';
|
|
6
|
+
import { FeishuApiService } from './services/feishuApiService.js';
|
|
7
|
+
import { Logger } from './utils/logger.js';
|
|
8
|
+
import { DocumentIdSchema, ParentBlockIdSchema, BlockIdSchema, IndexSchema, StartIndexSchema, AlignSchema, AlignSchemaWithValidation, TextElementsArraySchema, CodeLanguageSchema, CodeWrapSchema, BlockConfigSchema } from './types/feishuSchema.js';
|
|
134
9
|
export class FeishuMcpServer {
|
|
135
|
-
constructor(
|
|
10
|
+
constructor() {
|
|
136
11
|
Object.defineProperty(this, "server", {
|
|
137
12
|
enumerable: true,
|
|
138
13
|
configurable: true,
|
|
@@ -151,11 +26,10 @@ export class FeishuMcpServer {
|
|
|
151
26
|
writable: true,
|
|
152
27
|
value: null
|
|
153
28
|
});
|
|
154
|
-
// 详细记录飞书配置状态
|
|
155
|
-
Logger.log(`飞书配置已提供 - AppID: ${feishuConfig.appId.substring(0, 4)}...${feishuConfig.appId.substring(feishuConfig.appId.length - 4)}, AppSecret: ${feishuConfig.appSecret.substring(0, 4)}...${feishuConfig.appSecret.substring(feishuConfig.appSecret.length - 4)}`);
|
|
156
29
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
30
|
+
// 使用单例模式获取飞书服务实例
|
|
31
|
+
this.feishuService = FeishuApiService.getInstance();
|
|
32
|
+
Logger.info('飞书服务初始化成功');
|
|
159
33
|
}
|
|
160
34
|
catch (error) {
|
|
161
35
|
Logger.error('飞书服务初始化失败:', error);
|
|
@@ -179,12 +53,12 @@ export class FeishuMcpServer {
|
|
|
179
53
|
folderToken: z.string().describe('Folder token (required). Specifies where to create the document. Format is an alphanumeric string like "doxcnOu1ZKYH4RtX1Y5XwL5WGRh".'),
|
|
180
54
|
}, async ({ title, folderToken }) => {
|
|
181
55
|
try {
|
|
182
|
-
Logger.
|
|
56
|
+
Logger.info(`开始创建飞书文档,标题: ${title}${folderToken ? `,文件夹Token: ${folderToken}` : ',使用默认文件夹'}`);
|
|
183
57
|
const newDoc = await this.feishuService?.createDocument(title, folderToken);
|
|
184
58
|
if (!newDoc) {
|
|
185
59
|
throw new Error('创建文档失败,未返回文档信息');
|
|
186
60
|
}
|
|
187
|
-
Logger.
|
|
61
|
+
Logger.info(`飞书文档创建成功,文档ID: ${newDoc.objToken || newDoc.document_id}`);
|
|
188
62
|
return {
|
|
189
63
|
content: [{ type: 'text', text: JSON.stringify(newDoc, null, 2) }],
|
|
190
64
|
};
|
|
@@ -198,33 +72,33 @@ export class FeishuMcpServer {
|
|
|
198
72
|
}
|
|
199
73
|
});
|
|
200
74
|
// 添加获取飞书文档信息工具
|
|
201
|
-
this.server.tool(
|
|
202
|
-
documentId:
|
|
75
|
+
this.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.', {
|
|
76
|
+
documentId: DocumentIdSchema,
|
|
203
77
|
}, async ({ documentId }) => {
|
|
204
78
|
try {
|
|
205
79
|
if (!this.feishuService) {
|
|
206
80
|
return {
|
|
207
|
-
content: [{ type:
|
|
81
|
+
content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
|
|
208
82
|
};
|
|
209
83
|
}
|
|
210
|
-
Logger.
|
|
84
|
+
Logger.info(`开始获取飞书文档信息,文档ID: ${documentId}`);
|
|
211
85
|
const docInfo = await this.feishuService.getDocumentInfo(documentId);
|
|
212
|
-
Logger.
|
|
86
|
+
Logger.info(`飞书文档信息获取成功,标题: ${docInfo.title}`);
|
|
213
87
|
return {
|
|
214
|
-
content: [{ type:
|
|
88
|
+
content: [{ type: 'text', text: JSON.stringify(docInfo, null, 2) }],
|
|
215
89
|
};
|
|
216
90
|
}
|
|
217
91
|
catch (error) {
|
|
218
92
|
Logger.error(`获取飞书文档信息失败:`, error);
|
|
219
|
-
const errorMessage = formatErrorMessage(error);
|
|
93
|
+
const errorMessage = formatErrorMessage(error, '获取飞书文档信息失败');
|
|
220
94
|
return {
|
|
221
|
-
content: [{ type:
|
|
95
|
+
content: [{ type: 'text', text: errorMessage }],
|
|
222
96
|
};
|
|
223
97
|
}
|
|
224
98
|
});
|
|
225
99
|
// 添加获取飞书文档内容工具
|
|
226
|
-
this.server.tool('
|
|
227
|
-
documentId:
|
|
100
|
+
this.server.tool('get_feishu_document_content', 'Retrieves the plain text content of a Feishu document. Ideal for content analysis, processing, or when you need to extract text without formatting. The content maintains the document structure but without styling. Note: For Feishu wiki links (https://xxx.feishu.cn/wiki/xxx) you must first use convert_feishu_wiki_to_document_id tool to obtain a compatible document ID.', {
|
|
101
|
+
documentId: DocumentIdSchema,
|
|
228
102
|
lang: z.number().optional().default(0).describe('Language code (optional). Default is 0 (Chinese). Use 1 for English if available.'),
|
|
229
103
|
}, async ({ documentId, lang }) => {
|
|
230
104
|
try {
|
|
@@ -233,9 +107,9 @@ export class FeishuMcpServer {
|
|
|
233
107
|
content: [{ type: 'text', text: 'Feishu service is not initialized. Please check the configuration' }],
|
|
234
108
|
};
|
|
235
109
|
}
|
|
236
|
-
Logger.
|
|
110
|
+
Logger.info(`开始获取飞书文档内容,文档ID: ${documentId},语言: ${lang}`);
|
|
237
111
|
const content = await this.feishuService.getDocumentContent(documentId, lang);
|
|
238
|
-
Logger.
|
|
112
|
+
Logger.info(`飞书文档内容获取成功,内容长度: ${content.length}字符`);
|
|
239
113
|
return {
|
|
240
114
|
content: [{ type: 'text', text: content }],
|
|
241
115
|
};
|
|
@@ -249,8 +123,8 @@ export class FeishuMcpServer {
|
|
|
249
123
|
}
|
|
250
124
|
});
|
|
251
125
|
// 添加获取飞书文档块工具
|
|
252
|
-
this.server.tool('
|
|
253
|
-
documentId:
|
|
126
|
+
this.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) you must first use convert_feishu_wiki_to_document_id tool to obtain a compatible document ID.', {
|
|
127
|
+
documentId: DocumentIdSchema,
|
|
254
128
|
pageSize: z.number().optional().default(500).describe('Number of blocks per page (optional). Default is 500. Used for paginating large documents. Increase for more blocks at once, decrease for faster response with fewer blocks.'),
|
|
255
129
|
}, async ({ documentId, pageSize }) => {
|
|
256
130
|
try {
|
|
@@ -259,9 +133,9 @@ export class FeishuMcpServer {
|
|
|
259
133
|
content: [{ type: 'text', text: 'Feishu service is not initialized. Please check the configuration' }],
|
|
260
134
|
};
|
|
261
135
|
}
|
|
262
|
-
Logger.
|
|
136
|
+
Logger.info(`开始获取飞书文档块,文档ID: ${documentId},页大小: ${pageSize}`);
|
|
263
137
|
const blocks = await this.feishuService.getDocumentBlocks(documentId, pageSize);
|
|
264
|
-
Logger.
|
|
138
|
+
Logger.info(`飞书文档块获取成功,共 ${blocks.length} 个块`);
|
|
265
139
|
return {
|
|
266
140
|
content: [{ type: 'text', text: JSON.stringify(blocks, null, 2) }],
|
|
267
141
|
};
|
|
@@ -275,9 +149,9 @@ export class FeishuMcpServer {
|
|
|
275
149
|
}
|
|
276
150
|
});
|
|
277
151
|
// 添加获取块内容工具
|
|
278
|
-
this.server.tool('get_feishu_block_content', 'Retrieves the detailed content and structure of a specific block in a Feishu document. Useful for inspecting block properties, formatting, and content, especially before making updates or for debugging purposes.', {
|
|
279
|
-
documentId:
|
|
280
|
-
blockId:
|
|
152
|
+
this.server.tool('get_feishu_block_content', 'Retrieves the detailed content and structure of a specific block in a Feishu document. Useful for inspecting block properties, formatting, and content, especially before making updates or for debugging purposes. Note: For Feishu wiki links (https://xxx.feishu.cn/wiki/xxx) you must first use convert_feishu_wiki_to_document_id tool to obtain a compatible document ID.', {
|
|
153
|
+
documentId: DocumentIdSchema,
|
|
154
|
+
blockId: BlockIdSchema,
|
|
281
155
|
}, async ({ documentId, blockId }) => {
|
|
282
156
|
try {
|
|
283
157
|
if (!this.feishuService) {
|
|
@@ -285,9 +159,9 @@ export class FeishuMcpServer {
|
|
|
285
159
|
content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
|
|
286
160
|
};
|
|
287
161
|
}
|
|
288
|
-
Logger.
|
|
162
|
+
Logger.info(`开始获取飞书块内容,文档ID: ${documentId},块ID: ${blockId}`);
|
|
289
163
|
const blockContent = await this.feishuService.getBlockContent(documentId, blockId);
|
|
290
|
-
Logger.
|
|
164
|
+
Logger.info(`飞书块内容获取成功,块类型: ${blockContent.block_type}`);
|
|
291
165
|
return {
|
|
292
166
|
content: [{ type: 'text', text: JSON.stringify(blockContent, null, 2) }],
|
|
293
167
|
};
|
|
@@ -301,25 +175,10 @@ export class FeishuMcpServer {
|
|
|
301
175
|
}
|
|
302
176
|
});
|
|
303
177
|
// 添加更新块文本内容工具
|
|
304
|
-
this.server.tool('update_feishu_block_text', 'Updates the text content and styling of a specific block in a Feishu document. Can be used to modify content in existing text, code, or heading blocks while preserving the block type and other properties.', {
|
|
305
|
-
documentId:
|
|
306
|
-
blockId:
|
|
307
|
-
textElements:
|
|
308
|
-
text: z.string().describe('Text content. Provide plain text without markdown syntax; use the style object for formatting.'),
|
|
309
|
-
style: z.object({
|
|
310
|
-
bold: z.boolean().optional().describe('Whether to make text bold. Default is false, equivalent to **text** in Markdown.'),
|
|
311
|
-
italic: z.boolean().optional().describe('Whether to make text italic. Default is false, equivalent to *text* in Markdown.'),
|
|
312
|
-
underline: z.boolean().optional().describe('Whether to add underline. Default is false.'),
|
|
313
|
-
strikethrough: z.boolean().optional().describe('Whether to add strikethrough. Default is false, equivalent to ~~text~~ in Markdown.'),
|
|
314
|
-
inline_code: z.boolean().optional().describe('Whether to format as inline code. Default is false, equivalent to `code` in Markdown.'),
|
|
315
|
-
text_color: z.number().optional().refine(val => !val || (val >= 1 && val <= 7), {
|
|
316
|
-
message: "Text color must be between 1 and 7 inclusive"
|
|
317
|
-
}).describe('Text color value. Default is 0 (black). Available values are only: 1 (gray), 2 (brown), 3 (orange), 4 (yellow), 5 (green), 6 (blue), 7 (purple). Values outside this range will cause an error.'),
|
|
318
|
-
background_color: z.number().optional().refine(val => !val || (val >= 1 && val <= 7), {
|
|
319
|
-
message: "Background color must be between 1 and 7 inclusive"
|
|
320
|
-
}).describe('Background color value. Available values are only: 1 (gray), 2 (brown), 3 (orange), 4 (yellow), 5 (green), 6 (blue), 7 (purple). Values outside this range will cause an error.')
|
|
321
|
-
}).optional().describe('Text style settings. Explicitly set style properties instead of relying on Markdown syntax conversion.')
|
|
322
|
-
})).describe('Array of text content objects. A block can contain multiple text segments with different styles. Example: [{text:"Hello",style:{bold:true}},{text:" World",style:{italic:true}}]'),
|
|
178
|
+
this.server.tool('update_feishu_block_text', 'Updates the text content and styling of a specific block in a Feishu document. Can be used to modify content in existing text, code, or heading blocks while preserving the block type and other properties. Note: For Feishu wiki links (https://xxx.feishu.cn/wiki/xxx) you must first use convert_feishu_wiki_to_document_id tool to obtain a compatible document ID.', {
|
|
179
|
+
documentId: DocumentIdSchema,
|
|
180
|
+
blockId: BlockIdSchema,
|
|
181
|
+
textElements: TextElementsArraySchema,
|
|
323
182
|
}, async ({ documentId, blockId, textElements }) => {
|
|
324
183
|
try {
|
|
325
184
|
if (!this.feishuService) {
|
|
@@ -327,9 +186,9 @@ export class FeishuMcpServer {
|
|
|
327
186
|
content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
|
|
328
187
|
};
|
|
329
188
|
}
|
|
330
|
-
Logger.
|
|
189
|
+
Logger.info(`开始更新飞书块文本内容,文档ID: ${documentId},块ID: ${blockId}`);
|
|
331
190
|
const result = await this.feishuService.updateBlockTextContent(documentId, blockId, textElements);
|
|
332
|
-
Logger.
|
|
191
|
+
Logger.info(`飞书块文本内容更新成功`);
|
|
333
192
|
return {
|
|
334
193
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
335
194
|
};
|
|
@@ -343,61 +202,11 @@ export class FeishuMcpServer {
|
|
|
343
202
|
}
|
|
344
203
|
});
|
|
345
204
|
// 添加通用飞书块创建工具(支持文本、代码、标题)
|
|
346
|
-
this.server.tool('
|
|
347
|
-
documentId:
|
|
348
|
-
parentBlockId:
|
|
349
|
-
startIndex:
|
|
350
|
-
blocks: z.array(
|
|
351
|
-
blockType: z.enum(['text', 'code', 'heading', 'list']).describe("Block type (required): 'text', 'code', 'heading', or 'list'. Choose based on the content type you need to create."),
|
|
352
|
-
options: z.union([
|
|
353
|
-
z.object({
|
|
354
|
-
text: z.object({
|
|
355
|
-
textStyles: z.array(z.object({
|
|
356
|
-
text: z.string().describe('Text segment content. The actual text to display.'),
|
|
357
|
-
style: z.object({
|
|
358
|
-
bold: z.boolean().optional().describe('Whether to make text bold. Default is false, equivalent to **text** in Markdown.'),
|
|
359
|
-
italic: z.boolean().optional().describe('Whether to make text italic. Default is false, equivalent to *text* in Markdown.'),
|
|
360
|
-
underline: z.boolean().optional().describe('Whether to add underline. Default is false.'),
|
|
361
|
-
strikethrough: z.boolean().optional().describe('Whether to add strikethrough. Default is false, equivalent to ~~text~~ in Markdown.'),
|
|
362
|
-
inline_code: z.boolean().optional().describe('Whether to format as inline code. Default is false, equivalent to `code` in Markdown.'),
|
|
363
|
-
text_color: z.number().optional().refine(val => !val || (val >= 1 && val <= 7), {
|
|
364
|
-
message: "Text color must be between 1 and 7 inclusive"
|
|
365
|
-
}).describe('Text color value. Default is 0 (black). Available values are only: 1 (gray), 2 (brown), 3 (orange), 4 (yellow), 5 (green), 6 (blue), 7 (purple). Values outside this range will cause an error.'),
|
|
366
|
-
background_color: z.number().optional().refine(val => !val || (val >= 1 && val <= 7), {
|
|
367
|
-
message: "Background color must be between 1 and 7 inclusive"
|
|
368
|
-
}).describe('Background color value. Available values are only: 1 (gray), 2 (brown), 3 (orange), 4 (yellow), 5 (green), 6 (blue), 7 (purple). Values outside this range will cause an error.')
|
|
369
|
-
}).optional().describe('Text style settings. Explicitly set style properties instead of relying on Markdown syntax conversion.'),
|
|
370
|
-
})).describe('Array of text content objects with styles. A block can contain multiple text segments with different styles. Example: [{text:"Hello",style:{bold:true}},{text:" World",style:{italic:true}}]'),
|
|
371
|
-
align: z.number().optional().default(1).describe('Text alignment: 1 for left (default), 2 for center, 3 for right.'),
|
|
372
|
-
}).describe("Text block options. Only used when blockType is 'text'."),
|
|
373
|
-
}),
|
|
374
|
-
z.object({
|
|
375
|
-
code: z.object({
|
|
376
|
-
code: z.string().describe('Code content. The complete code text to display.'),
|
|
377
|
-
language: z.number().optional().default(0).describe('Programming language code. Default is 0 (auto-detect). See documentation for full list of language codes.'),
|
|
378
|
-
wrap: z.boolean().optional().default(false).describe('Whether to enable automatic line wrapping. Default is false.'),
|
|
379
|
-
}).describe("Code block options. Only used when blockType is 'code'."),
|
|
380
|
-
}),
|
|
381
|
-
z.object({
|
|
382
|
-
heading: z.object({
|
|
383
|
-
level: z.number().min(1).max(9).describe('Heading level from 1 to 9, where 1 is the largest (h1) and 9 is the smallest (h9).'),
|
|
384
|
-
content: z.string().describe('Heading text content. The actual text of the heading.'),
|
|
385
|
-
align: z.number().optional().default(1).refine(val => val === 1 || val === 2 || val === 3, {
|
|
386
|
-
message: "Alignment must be one of: 1 (left), 2 (center), or 3 (right)"
|
|
387
|
-
}).describe('Text alignment: 1 for left (default), 2 for center, 3 for right. Only these three values are allowed.'),
|
|
388
|
-
}).describe("Heading block options. Only used when blockType is 'heading'."),
|
|
389
|
-
}),
|
|
390
|
-
z.object({
|
|
391
|
-
list: z.object({
|
|
392
|
-
content: z.string().describe('List item content. The actual text of the list item.'),
|
|
393
|
-
isOrdered: z.boolean().optional().default(false).describe('Whether this is an ordered (numbered) list item. Default is false (bullet point/unordered).'),
|
|
394
|
-
align: z.number().optional().default(1).refine(val => val === 1 || val === 2 || val === 3, {
|
|
395
|
-
message: "Alignment must be one of: 1 (left), 2 (center), or 3 (right)"
|
|
396
|
-
}).describe('Text alignment: 1 for left (default), 2 for center, 3 for right. Only these three values are allowed.'),
|
|
397
|
-
}).describe("List block options. Only used when blockType is 'list'."),
|
|
398
|
-
}),
|
|
399
|
-
]).describe('Options for the specific block type. Must provide the corresponding options object based on blockType.'),
|
|
400
|
-
})).max(50).describe('Array of block configurations (required). Each element contains blockType and options properties. Example: [{blockType:"text",options:{text:{textStyles:[{text:"Hello",style:{bold:true}}]}}},{blockType:"code",options:{code:{code:"console.log(\'Hello\')",language:30}}}]. Maximum 50 blocks per call.'),
|
|
205
|
+
this.server.tool('batch_create_feishu_blocks', 'RECOMMENDED: Creates multiple blocks of different types (text, code, heading, list) in a single efficient API call. This tool should be PREFERRED OVER individual block creation tools when creating multiple consecutive blocks at the same position. Significantly improves performance and reduces API calls by up to 90% compared to creating blocks individually. AUTOMATICALLY handles batching for large number of blocks (>50) by splitting into multiple requests. For specific block positioning at different locations, use individual block creation tools instead. For error recovery, use get_feishu_document_blocks to check the document state. Note: For Feishu wiki links (https://xxx.feishu.cn/wiki/xxx) you must first use convert_feishu_wiki_to_document_id tool to obtain a compatible document ID.', {
|
|
206
|
+
documentId: DocumentIdSchema,
|
|
207
|
+
parentBlockId: ParentBlockIdSchema,
|
|
208
|
+
startIndex: StartIndexSchema,
|
|
209
|
+
blocks: z.array(BlockConfigSchema).describe('Array of block configurations (required). Each element contains blockType and options properties. Example: [{blockType:"text",options:{text:{textStyles:[{text:"Hello",style:{bold:true}}]}}},{blockType:"code",options:{code:{code:"console.log(\'Hello\')",language:30}}}]. Handles any number of blocks by automatically batching in groups of 50.'),
|
|
401
210
|
}, async ({ documentId, parentBlockId, startIndex = 0, blocks }) => {
|
|
402
211
|
try {
|
|
403
212
|
if (!this.feishuService) {
|
|
@@ -410,128 +219,119 @@ export class FeishuMcpServer {
|
|
|
410
219
|
],
|
|
411
220
|
};
|
|
412
221
|
}
|
|
413
|
-
|
|
222
|
+
// 如果块数量不超过50,直接调用一次API
|
|
223
|
+
if (blocks.length <= 50) {
|
|
224
|
+
Logger.info(`开始批量创建飞书块,文档ID: ${documentId},父块ID: ${parentBlockId},块数量: ${blocks.length},起始插入位置: ${startIndex}`);
|
|
225
|
+
// 准备要创建的块内容数组
|
|
226
|
+
const blockContents = [];
|
|
227
|
+
// 处理每个块配置
|
|
228
|
+
for (const blockConfig of blocks) {
|
|
229
|
+
const { blockType, options = {} } = blockConfig;
|
|
230
|
+
// 创建块内容
|
|
231
|
+
const blockContent = this.feishuService.createBlockContent(blockType, options);
|
|
232
|
+
if (blockContent) {
|
|
233
|
+
blockContents.push(blockContent);
|
|
234
|
+
Logger.info(`已准备${blockType}块,内容: ${JSON.stringify(blockContent).substring(0, 100)}...`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// 批量创建所有块
|
|
238
|
+
const result = await this.feishuService.createDocumentBlocks(documentId, parentBlockId, blockContents, startIndex);
|
|
239
|
+
Logger.info(`飞书块批量创建成功,共创建 ${blockContents.length} 个块`);
|
|
414
240
|
return {
|
|
415
|
-
content: [{
|
|
416
|
-
type: 'text',
|
|
417
|
-
text: '错误: 每次调用最多只能创建50个块。请分批次创建或减少块数量。'
|
|
418
|
-
}],
|
|
241
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
419
242
|
};
|
|
420
243
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
let
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
case 'code':
|
|
446
|
-
// 处理代码块
|
|
447
|
-
{
|
|
448
|
-
// 类型检查,确保options包含code属性
|
|
449
|
-
if ('code' in options && options.code) {
|
|
450
|
-
const codeOptions = options.code;
|
|
451
|
-
const codeContent = codeOptions.code || '';
|
|
452
|
-
const language = codeOptions.language || 0;
|
|
453
|
-
const wrap = codeOptions.wrap || false;
|
|
454
|
-
blockContent = this.feishuService.createCodeBlockContent(codeContent, language, wrap);
|
|
455
|
-
}
|
|
456
|
-
break;
|
|
457
|
-
}
|
|
458
|
-
case 'heading':
|
|
459
|
-
// 处理标题块
|
|
460
|
-
{
|
|
461
|
-
// 类型检查,确保options包含heading属性
|
|
462
|
-
if ('heading' in options && options.heading) {
|
|
463
|
-
const headingOptions = options.heading;
|
|
464
|
-
if (headingOptions.content) {
|
|
465
|
-
const headingContent = headingOptions.content;
|
|
466
|
-
const level = headingOptions.level || 1;
|
|
467
|
-
// 确保对齐方式值在合法范围内
|
|
468
|
-
const headingAlign = (headingOptions.align === 1 || headingOptions.align === 2 || headingOptions.align === 3)
|
|
469
|
-
? headingOptions.align : 1;
|
|
470
|
-
blockContent = this.feishuService.createHeadingBlockContent(headingContent, level, headingAlign);
|
|
471
|
-
}
|
|
244
|
+
else {
|
|
245
|
+
// 如果块数量超过50,需要分批处理
|
|
246
|
+
Logger.info(`块数量(${blocks.length})超过50,将分批创建`);
|
|
247
|
+
const batchSize = 50; // 每批最大50个
|
|
248
|
+
const totalBatches = Math.ceil(blocks.length / batchSize);
|
|
249
|
+
const results = [];
|
|
250
|
+
let currentStartIndex = startIndex;
|
|
251
|
+
let createdBlocksCount = 0;
|
|
252
|
+
let allBatchesSuccess = true;
|
|
253
|
+
// 分批创建块
|
|
254
|
+
for (let batchNum = 0; batchNum < totalBatches; batchNum++) {
|
|
255
|
+
const batchStart = batchNum * batchSize;
|
|
256
|
+
const batchEnd = Math.min((batchNum + 1) * batchSize, blocks.length);
|
|
257
|
+
const currentBatch = blocks.slice(batchStart, batchEnd);
|
|
258
|
+
Logger.info(`处理第 ${batchNum + 1}/${totalBatches} 批,起始位置: ${currentStartIndex},块数量: ${currentBatch.length}`);
|
|
259
|
+
try {
|
|
260
|
+
// 准备当前批次的块内容
|
|
261
|
+
const batchBlockContents = [];
|
|
262
|
+
for (const blockConfig of currentBatch) {
|
|
263
|
+
const { blockType, options = {} } = blockConfig;
|
|
264
|
+
const blockContent = this.feishuService.createBlockContent(blockType, options);
|
|
265
|
+
if (blockContent) {
|
|
266
|
+
batchBlockContents.push(blockContent);
|
|
472
267
|
}
|
|
473
|
-
break;
|
|
474
268
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
269
|
+
// 批量创建当前批次的块
|
|
270
|
+
const batchResult = await this.feishuService.createDocumentBlocks(documentId, parentBlockId, batchBlockContents, currentStartIndex);
|
|
271
|
+
results.push(batchResult);
|
|
272
|
+
// 计算下一批的起始位置(当前位置+已创建块数量)
|
|
273
|
+
// 注意:每批成功创建后,需要将起始索引更新为当前索引 + 已创建块数量
|
|
274
|
+
createdBlocksCount += batchBlockContents.length;
|
|
275
|
+
currentStartIndex = startIndex + createdBlocksCount;
|
|
276
|
+
Logger.info(`第 ${batchNum + 1}/${totalBatches} 批创建成功,当前已创建 ${createdBlocksCount} 个块`);
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
Logger.error(`第 ${batchNum + 1}/${totalBatches} 批创建失败:`, error);
|
|
280
|
+
allBatchesSuccess = false;
|
|
281
|
+
// 如果有批次失败,返回详细错误信息
|
|
282
|
+
const errorMessage = formatErrorMessage(error);
|
|
283
|
+
return {
|
|
284
|
+
content: [
|
|
285
|
+
{
|
|
286
|
+
type: 'text',
|
|
287
|
+
text: `批量创建飞书块部分失败:第 ${batchNum + 1}/${totalBatches} 批处理时出错。\n\n` +
|
|
288
|
+
`已成功创建 ${createdBlocksCount} 个块,但还有 ${blocks.length - createdBlocksCount} 个块未能创建。\n\n` +
|
|
289
|
+
`错误信息: ${errorMessage}\n\n` +
|
|
290
|
+
`建议使用 get_feishu_document_blocks 工具获取文档最新状态,确认已创建的内容,然后从索引位置 ${currentStartIndex} 继续创建剩余块。`
|
|
488
291
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
292
|
+
],
|
|
293
|
+
};
|
|
294
|
+
}
|
|
492
295
|
}
|
|
493
|
-
if (
|
|
494
|
-
|
|
495
|
-
|
|
296
|
+
if (allBatchesSuccess) {
|
|
297
|
+
Logger.info(`所有批次创建成功,共创建 ${createdBlocksCount} 个块`);
|
|
298
|
+
return {
|
|
299
|
+
content: [
|
|
300
|
+
{
|
|
301
|
+
type: 'text',
|
|
302
|
+
text: `所有飞书块创建成功,共分 ${totalBatches} 批创建了 ${createdBlocksCount} 个块。\n\n` +
|
|
303
|
+
`最后一批结果: ${JSON.stringify(results[results.length - 1], null, 2)}`
|
|
304
|
+
}
|
|
305
|
+
],
|
|
306
|
+
};
|
|
496
307
|
}
|
|
497
308
|
}
|
|
498
|
-
//
|
|
499
|
-
const result = await this.feishuService.createDocumentBlocks(documentId, parentBlockId, blockContents, startIndex);
|
|
500
|
-
Logger.log(`飞书块批量创建成功,共创建 ${blockContents.length} 个块`);
|
|
309
|
+
// 这个return语句是为了避免TypeScript错误,实际上代码永远不会执行到这里
|
|
501
310
|
return {
|
|
502
|
-
content: [{ type: 'text', text:
|
|
311
|
+
content: [{ type: 'text', text: '操作完成' }],
|
|
503
312
|
};
|
|
504
313
|
}
|
|
505
314
|
catch (error) {
|
|
506
315
|
Logger.error(`批量创建飞书块失败:`, error);
|
|
507
316
|
const errorMessage = formatErrorMessage(error);
|
|
508
317
|
return {
|
|
509
|
-
content: [
|
|
318
|
+
content: [
|
|
319
|
+
{
|
|
320
|
+
type: 'text',
|
|
321
|
+
text: `批量创建飞书块失败: ${errorMessage}\n\n` +
|
|
322
|
+
`建议使用 get_feishu_document_blocks 工具获取文档当前状态,确认是否有部分内容已创建成功。`
|
|
323
|
+
}
|
|
324
|
+
],
|
|
510
325
|
};
|
|
511
326
|
}
|
|
512
327
|
});
|
|
513
328
|
// 添加创建飞书文本块工具
|
|
514
|
-
this.server.tool("
|
|
515
|
-
documentId:
|
|
516
|
-
parentBlockId:
|
|
517
|
-
textContents:
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
bold: z.boolean().optional().describe("Whether to make text bold. Default is false, equivalent to **text** in Markdown."),
|
|
521
|
-
italic: z.boolean().optional().describe("Whether to make text italic. Default is false, equivalent to *text* in Markdown."),
|
|
522
|
-
underline: z.boolean().optional().describe("Whether to add underline. Default is false."),
|
|
523
|
-
strikethrough: z.boolean().optional().describe("Whether to add strikethrough. Default is false, equivalent to ~~text~~ in Markdown."),
|
|
524
|
-
inline_code: z.boolean().optional().describe("Whether to format as inline code. Default is false, equivalent to `code` in Markdown."),
|
|
525
|
-
text_color: z.number().optional().refine(val => !val || (val >= 1 && val <= 7), {
|
|
526
|
-
message: "Text color must be between 1 and 7 inclusive"
|
|
527
|
-
}).describe("Text color value. Default is 0 (black). Available values are only: 1 (gray), 2 (brown), 3 (orange), 4 (yellow), 5 (green), 6 (blue), 7 (purple). Values outside this range will cause an error."),
|
|
528
|
-
background_color: z.number().optional().refine(val => !val || (val >= 1 && val <= 7), {
|
|
529
|
-
message: "Background color must be between 1 and 7 inclusive"
|
|
530
|
-
}).describe('Background color value. Available values are only: 1 (gray), 2 (brown), 3 (orange), 4 (yellow), 5 (green), 6 (blue), 7 (purple). Values outside this range will cause an error.')
|
|
531
|
-
}).optional().describe("Text style settings. Explicitly set style properties instead of relying on Markdown syntax conversion.")
|
|
532
|
-
})).describe("Array of text content objects. A block can contain multiple text segments with different styles. Example: [{text:'Hello',style:{bold:true}},{text:' World',style:{italic:true}}]"),
|
|
533
|
-
align: z.number().optional().default(1).describe("Text alignment: 1 for left (default), 2 for center, 3 for right."),
|
|
534
|
-
index: z.number().describe("Insertion position index (required). Specifies where the block should be inserted. Use 0 to insert at the beginning. Use get_feishu_doc_blocks tool to understand document structure if unsure. For consecutive insertions, calculate next index as previous index + 1.")
|
|
329
|
+
this.server.tool("create_feishu_text_block", "Creates a new text block with precise style control. Unlike markdown-based formatting, this tool lets you explicitly set text styles for each text segment. Ideal for formatted documents where exact styling control is needed. NOTE: If creating multiple blocks at once, use batch_create_feishu_blocks tool instead for better efficiency. Note: For Feishu wiki links (https://xxx.feishu.cn/wiki/xxx) you must first use convert_feishu_wiki_to_document_id tool to obtain a compatible document ID.", {
|
|
330
|
+
documentId: DocumentIdSchema,
|
|
331
|
+
parentBlockId: ParentBlockIdSchema,
|
|
332
|
+
textContents: TextElementsArraySchema,
|
|
333
|
+
align: AlignSchema,
|
|
334
|
+
index: IndexSchema
|
|
535
335
|
}, async ({ documentId, parentBlockId, textContents, align = 1, index }) => {
|
|
536
336
|
try {
|
|
537
337
|
if (!this.feishuService) {
|
|
@@ -539,9 +339,9 @@ export class FeishuMcpServer {
|
|
|
539
339
|
content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
|
|
540
340
|
};
|
|
541
341
|
}
|
|
542
|
-
Logger.
|
|
342
|
+
Logger.info(`开始创建飞书文本块,文档ID: ${documentId},父块ID: ${parentBlockId},对齐方式: ${align},插入位置: ${index}`);
|
|
543
343
|
const result = await this.feishuService.createTextBlock(documentId, parentBlockId, textContents, align, index);
|
|
544
|
-
Logger.
|
|
344
|
+
Logger.info(`飞书文本块创建成功`);
|
|
545
345
|
return {
|
|
546
346
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
547
347
|
};
|
|
@@ -555,23 +355,23 @@ export class FeishuMcpServer {
|
|
|
555
355
|
}
|
|
556
356
|
});
|
|
557
357
|
// 添加创建飞书代码块工具
|
|
558
|
-
this.server.tool("
|
|
559
|
-
documentId:
|
|
560
|
-
parentBlockId:
|
|
358
|
+
this.server.tool("create_feishu_code_block", "Creates a new code block with syntax highlighting and formatting options. Ideal for technical documentation, tutorials, or displaying code examples with proper formatting and language-specific highlighting. NOTE: If creating multiple blocks at once, use batch_create_feishu_blocks tool instead for better efficiency. Note: For Feishu wiki links (https://xxx.feishu.cn/wiki/xxx) you must first use convert_feishu_wiki_to_document_id tool to obtain a compatible document ID.", {
|
|
359
|
+
documentId: DocumentIdSchema,
|
|
360
|
+
parentBlockId: ParentBlockIdSchema,
|
|
561
361
|
code: z.string().describe("Code content (required). The complete code text to display."),
|
|
562
|
-
language:
|
|
563
|
-
wrap:
|
|
564
|
-
index:
|
|
565
|
-
}, async ({ documentId, parentBlockId, code, language =
|
|
362
|
+
language: CodeLanguageSchema,
|
|
363
|
+
wrap: CodeWrapSchema,
|
|
364
|
+
index: IndexSchema
|
|
365
|
+
}, async ({ documentId, parentBlockId, code, language = 1, wrap = false, index = 0 }) => {
|
|
566
366
|
try {
|
|
567
367
|
if (!this.feishuService) {
|
|
568
368
|
return {
|
|
569
369
|
content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
|
|
570
370
|
};
|
|
571
371
|
}
|
|
572
|
-
Logger.
|
|
372
|
+
Logger.info(`开始创建飞书代码块,文档ID: ${documentId},父块ID: ${parentBlockId},语言: ${language},自动换行: ${wrap},插入位置: ${index}`);
|
|
573
373
|
const result = await this.feishuService.createCodeBlock(documentId, parentBlockId, code, language, wrap, index);
|
|
574
|
-
Logger.
|
|
374
|
+
Logger.info(`飞书代码块创建成功`);
|
|
575
375
|
return {
|
|
576
376
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
577
377
|
};
|
|
@@ -585,15 +385,13 @@ export class FeishuMcpServer {
|
|
|
585
385
|
}
|
|
586
386
|
});
|
|
587
387
|
// 添加创建飞书标题块工具
|
|
588
|
-
this.server.tool("
|
|
589
|
-
documentId:
|
|
590
|
-
parentBlockId:
|
|
388
|
+
this.server.tool("create_feishu_heading_block", "Creates a heading block with customizable level and alignment. Use this tool to add section titles, chapter headings, or any hierarchical structure elements to your document. Supports nine heading levels for different emphasis needs. NOTE: If creating multiple blocks at once, use batch_create_feishu_blocks tool instead for better efficiency. Note: For Feishu wiki links (https://xxx.feishu.cn/wiki/xxx) you must first use convert_feishu_wiki_to_document_id tool to obtain a compatible document ID.", {
|
|
389
|
+
documentId: DocumentIdSchema,
|
|
390
|
+
parentBlockId: ParentBlockIdSchema,
|
|
591
391
|
level: z.number().min(1).max(9).describe("Heading level (required). Integer between 1 and 9, where 1 is the largest heading (h1) and 9 is the smallest (h9)."),
|
|
592
392
|
content: z.string().describe("Heading text content (required). The actual text of the heading."),
|
|
593
|
-
align:
|
|
594
|
-
|
|
595
|
-
}).describe("Text alignment (optional): 1 for left (default), 2 for center, 3 for right. Only these three values are allowed."),
|
|
596
|
-
index: z.number().describe("Insertion position index (required). Specifies where the block should be inserted. Use 0 to insert at the beginning. Use get_feishu_doc_blocks tool to understand document structure if unsure. For consecutive insertions, calculate next index as previous index + 1.")
|
|
393
|
+
align: AlignSchemaWithValidation,
|
|
394
|
+
index: IndexSchema
|
|
597
395
|
}, async ({ documentId, parentBlockId, level, content, align = 1, index = 0 }) => {
|
|
598
396
|
try {
|
|
599
397
|
if (!this.feishuService) {
|
|
@@ -607,9 +405,9 @@ export class FeishuMcpServer {
|
|
|
607
405
|
content: [{ type: "text", text: "错误: 对齐方式(align)参数必须是1(居左)、2(居中)或3(居右)中的一个值。" }],
|
|
608
406
|
};
|
|
609
407
|
}
|
|
610
|
-
Logger.
|
|
408
|
+
Logger.info(`开始创建飞书标题块,文档ID: ${documentId},父块ID: ${parentBlockId},标题级别: ${level},对齐方式: ${align},插入位置: ${index}`);
|
|
611
409
|
const result = await this.feishuService.createHeadingBlock(documentId, parentBlockId, content, level, index, align);
|
|
612
|
-
Logger.
|
|
410
|
+
Logger.info(`飞书标题块创建成功`);
|
|
613
411
|
return {
|
|
614
412
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
615
413
|
};
|
|
@@ -623,15 +421,13 @@ export class FeishuMcpServer {
|
|
|
623
421
|
}
|
|
624
422
|
});
|
|
625
423
|
// 添加创建飞书列表块工具
|
|
626
|
-
this.server.tool("
|
|
627
|
-
documentId:
|
|
628
|
-
parentBlockId:
|
|
424
|
+
this.server.tool("create_feishu_list_block", "Creates a list item block (either ordered or unordered). Perfect for creating hierarchical and structured content with bullet points or numbered lists. NOTE: If creating multiple blocks at once, use batch_create_feishu_blocks tool instead for better efficiency. Note: For Feishu wiki links (https://xxx.feishu.cn/wiki/xxx) you must first use convert_feishu_wiki_to_document_id tool to obtain a compatible document ID.", {
|
|
425
|
+
documentId: DocumentIdSchema,
|
|
426
|
+
parentBlockId: ParentBlockIdSchema,
|
|
629
427
|
content: z.string().describe("List item content (required). The actual text of the list item."),
|
|
630
428
|
isOrdered: z.boolean().optional().default(false).describe("Whether this is an ordered (numbered) list item. Default is false (bullet point/unordered)."),
|
|
631
|
-
align:
|
|
632
|
-
|
|
633
|
-
}).describe("Text alignment (optional): 1 for left (default), 2 for center, 3 for right. Only these three values are allowed."),
|
|
634
|
-
index: z.number().describe("Insertion position index (required). Specifies where the block should be inserted. Use 0 to insert at the beginning. Use get_feishu_doc_blocks tool to understand document structure if unsure. For consecutive insertions, calculate next index as previous index + 1.")
|
|
429
|
+
align: AlignSchemaWithValidation,
|
|
430
|
+
index: IndexSchema
|
|
635
431
|
}, async ({ documentId, parentBlockId, content, isOrdered = false, align = 1, index = 0 }) => {
|
|
636
432
|
try {
|
|
637
433
|
if (!this.feishuService) {
|
|
@@ -646,9 +442,9 @@ export class FeishuMcpServer {
|
|
|
646
442
|
};
|
|
647
443
|
}
|
|
648
444
|
const listType = isOrdered ? "有序" : "无序";
|
|
649
|
-
Logger.
|
|
445
|
+
Logger.info(`开始创建飞书${listType}列表块,文档ID: ${documentId},父块ID: ${parentBlockId},对齐方式: ${align},插入位置: ${index}`);
|
|
650
446
|
const result = await this.feishuService.createListBlock(documentId, parentBlockId, content, isOrdered, index, align);
|
|
651
|
-
Logger.
|
|
447
|
+
Logger.info(`飞书${listType}列表块创建成功`);
|
|
652
448
|
return {
|
|
653
449
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
654
450
|
};
|
|
@@ -661,16 +457,43 @@ export class FeishuMcpServer {
|
|
|
661
457
|
};
|
|
662
458
|
}
|
|
663
459
|
});
|
|
460
|
+
// 添加飞书Wiki文档ID转换工具
|
|
461
|
+
this.server.tool('convert_feishu_wiki_to_document_id', 'Converts a Feishu Wiki document link to a compatible document ID. This conversion is required before using wiki links with any other Feishu document tools.', {
|
|
462
|
+
wikiUrl: z.string().describe('Wiki URL or Token (required). Supports complete URL formats like https://xxx.feishu.cn/wiki/xxxxx or direct use of the Token portion'),
|
|
463
|
+
}, async ({ wikiUrl }) => {
|
|
464
|
+
try {
|
|
465
|
+
if (!this.feishuService) {
|
|
466
|
+
return {
|
|
467
|
+
content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
Logger.info(`开始转换Wiki文档链接,输入: ${wikiUrl}`);
|
|
471
|
+
const documentId = await this.feishuService.convertWikiToDocumentId(wikiUrl);
|
|
472
|
+
Logger.info(`Wiki文档转换成功,可用的文档ID为: ${documentId}`);
|
|
473
|
+
return {
|
|
474
|
+
content: [
|
|
475
|
+
{ type: 'text', text: `Converted Wiki link to Document ID: ${documentId}\n\nUse this Document ID with other Feishu document tools.` }
|
|
476
|
+
],
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
Logger.error(`转换Wiki文档链接失败:`, error);
|
|
481
|
+
const errorMessage = formatErrorMessage(error);
|
|
482
|
+
return {
|
|
483
|
+
content: [{ type: 'text', text: `转换Wiki文档链接失败: ${errorMessage}` }],
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
});
|
|
664
487
|
}
|
|
665
488
|
async connect(transport) {
|
|
666
489
|
await this.server.connect(transport);
|
|
667
|
-
Logger.
|
|
490
|
+
Logger.info = (...args) => {
|
|
668
491
|
this.server.server.sendLoggingMessage({ level: 'info', data: args });
|
|
669
492
|
};
|
|
670
493
|
Logger.error = (...args) => {
|
|
671
494
|
this.server.server.sendLoggingMessage({ level: 'error', data: args });
|
|
672
495
|
};
|
|
673
|
-
Logger.
|
|
496
|
+
Logger.info('Server connected and ready to process requests');
|
|
674
497
|
}
|
|
675
498
|
async startHttpServer(port) {
|
|
676
499
|
const app = express();
|
|
@@ -686,12 +509,12 @@ export class FeishuMcpServer {
|
|
|
686
509
|
}
|
|
687
510
|
await this.sseTransport.handlePostMessage(req, res);
|
|
688
511
|
});
|
|
689
|
-
Logger.
|
|
512
|
+
Logger.info = console.log;
|
|
690
513
|
Logger.error = console.error;
|
|
691
514
|
app.listen(port, () => {
|
|
692
|
-
Logger.
|
|
693
|
-
Logger.
|
|
694
|
-
Logger.
|
|
515
|
+
Logger.info(`HTTP server listening on port ${port}`);
|
|
516
|
+
Logger.info(`SSE endpoint available at http://localhost:${port}/sse`);
|
|
517
|
+
Logger.info(`Message endpoint available at http://localhost:${port}/messages`);
|
|
695
518
|
});
|
|
696
519
|
}
|
|
697
520
|
}
|