feishu-mcp 0.0.6 → 0.0.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/dist/server.js CHANGED
@@ -1,18 +1,14 @@
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
- export const Logger = {
7
- log: (...args) => {
8
- console.log(...args);
9
- },
10
- error: (...args) => {
11
- console.error(...args);
12
- },
13
- };
5
+ import { formatErrorMessage } from './utils/error.js';
6
+ import { FeishuApiService } from './services/feishuApiService.js';
7
+ import { Logger } from './utils/logger.js';
8
+ import { detectMimeType } from './utils/document.js';
9
+ import { DocumentIdSchema, ParentBlockIdSchema, BlockIdSchema, IndexSchema, StartIndexSchema, EndIndexSchema, AlignSchema, AlignSchemaWithValidation, TextElementsArraySchema, CodeLanguageSchema, CodeWrapSchema, BlockConfigSchema, MediaIdSchema, MediaExtraSchema, FolderTokenSchema, FolderNameSchema, OrderBySchema, DirectionSchema } from './types/feishuSchema.js';
14
10
  export class FeishuMcpServer {
15
- constructor(feishuConfig) {
11
+ constructor() {
16
12
  Object.defineProperty(this, "server", {
17
13
  enumerable: true,
18
14
  configurable: true,
@@ -31,11 +27,10 @@ export class FeishuMcpServer {
31
27
  writable: true,
32
28
  value: null
33
29
  });
34
- // 详细记录飞书配置状态
35
- 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)}`);
36
30
  try {
37
- this.feishuService = new FeishuService(feishuConfig.appId, feishuConfig.appSecret);
38
- Logger.log('飞书服务初始化成功');
31
+ // 使用单例模式获取飞书服务实例
32
+ this.feishuService = FeishuApiService.getInstance();
33
+ Logger.info('飞书服务初始化成功');
39
34
  }
40
35
  catch (error) {
41
36
  Logger.error('飞书服务初始化失败:', error);
@@ -54,399 +49,165 @@ export class FeishuMcpServer {
54
49
  }
55
50
  registerTools() {
56
51
  // 添加创建飞书文档工具
57
- this.server.tool('create_feishu_doc', 'Create a new Feishu document', {
58
- title: z.string().describe('Document title'),
59
- folderToken: z
60
- .string()
61
- .optional()
62
- .describe('Folder token where the document will be created. If not provided, the document will be created in the root directory'),
52
+ this.server.tool('create_feishu_document', 'Creates a new Feishu document and returns its information. Use this tool when you need to create a document from scratch with a specific title and folder location.', {
53
+ title: z.string().describe('Document title (required). This will be displayed in the Feishu document list and document header.'),
54
+ folderToken: z.string().describe('Folder token (required). Specifies where to create the document. Format is an alphanumeric string like "doxcnOu1ZKYH4RtX1Y5XwL5WGRh".'),
63
55
  }, async ({ title, folderToken }) => {
64
56
  try {
65
- Logger.log(`开始创建飞书文档,标题: ${title}${folderToken ? `,文件夹Token: ${folderToken}` : ''}`);
66
- // @ts-ignore
67
- const newDoc = await this.feishuService.createDocument(title, folderToken);
68
- Logger.log(`飞书文档创建成功,文档ID: ${newDoc?.objToken || newDoc?.document_id}`);
57
+ Logger.info(`开始创建飞书文档,标题: ${title}${folderToken ? `,文件夹Token: ${folderToken}` : ',使用默认文件夹'}`);
58
+ const newDoc = await this.feishuService?.createDocument(title, folderToken);
59
+ if (!newDoc) {
60
+ throw new Error('创建文档失败,未返回文档信息');
61
+ }
62
+ Logger.info(`飞书文档创建成功,文档ID: ${newDoc.objToken || newDoc.document_id}`);
69
63
  return {
70
64
  content: [{ type: 'text', text: JSON.stringify(newDoc, null, 2) }],
71
65
  };
72
66
  }
73
67
  catch (error) {
74
68
  Logger.error(`创建飞书文档失败:`, error);
69
+ const errorMessage = formatErrorMessage(error);
75
70
  return {
76
- content: [{ type: 'text', text: `创建飞书文档失败: ${error}` }],
71
+ content: [{ type: 'text', text: `创建飞书文档失败: ${errorMessage}` }],
77
72
  };
78
73
  }
79
74
  });
80
75
  // 添加获取飞书文档信息工具
81
- // this.server.tool(
82
- // "get_feishu_doc_info",
83
- // "Get basic information about a Feishu document",
84
- // {
85
- // documentId: z.string().describe("Document ID or URL. Supported formats:\n1. Standard document URL: https://xxx.feishu.cn/docs/xxx or https://xxx.feishu.cn/docx/xxx\n2. API URL: https://open.feishu.cn/open-apis/doc/v2/documents/xxx\n3. Direct document ID (e.g., JcKbdlokYoPIe0xDzJ1cduRXnRf)"),
86
- // },
87
- // async ({ documentId }) => {
88
- // try {
89
- // if (!this.feishuService) {
90
- // return {
91
- // content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
92
- // };
93
- // }
94
- // Logger.log(`开始获取飞书文档信息,文档ID: ${documentId}`);
95
- // const docInfo = await this.feishuService.getDocumentInfo(documentId);
96
- // Logger.log(`飞书文档信息获取成功,标题: ${docInfo.title}`);
97
- // return {
98
- // content: [{ type: "text", text: JSON.stringify(docInfo, null, 2) }],
99
- // };
100
- // } catch (error) {
101
- // Logger.error(`获取飞书文档信息失败:`, error);
102
- // return {
103
- // content: [{ type: "text", text: `获取飞书文档信息失败: ${error}` }],
104
- // };
105
- // }
106
- // },
107
- // );
76
+ 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.', {
77
+ documentId: DocumentIdSchema,
78
+ }, async ({ documentId }) => {
79
+ try {
80
+ if (!this.feishuService) {
81
+ return {
82
+ content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
83
+ };
84
+ }
85
+ Logger.info(`开始获取飞书文档信息,文档ID: ${documentId}`);
86
+ const docInfo = await this.feishuService.getDocumentInfo(documentId);
87
+ Logger.info(`飞书文档信息获取成功,标题: ${docInfo.title}`);
88
+ return {
89
+ content: [{ type: 'text', text: JSON.stringify(docInfo, null, 2) }],
90
+ };
91
+ }
92
+ catch (error) {
93
+ Logger.error(`获取飞书文档信息失败:`, error);
94
+ const errorMessage = formatErrorMessage(error, '获取飞书文档信息失败');
95
+ return {
96
+ content: [{ type: 'text', text: errorMessage }],
97
+ };
98
+ }
99
+ });
108
100
  // 添加获取飞书文档内容工具
109
- this.server.tool('get_feishu_doc_content', 'Get the plain text content of a Feishu document', {
110
- documentId: z
111
- .string()
112
- .describe('Document ID or URL. Supported formats:\n1. Standard document URL: https://xxx.feishu.cn/docs/xxx or https://xxx.feishu.cn/docx/xxx\n2. API URL: https://open.feishu.cn/open-apis/doc/v2/documents/xxx\n3. Direct document ID (e.g., JcKbdlokYoPIe0xDzJ1cduRXnRf)'),
113
- lang: z
114
- .number()
115
- .optional()
116
- .default(0)
117
- .describe('Language code. Default is 0 (Chinese)'),
101
+ 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.', {
102
+ documentId: DocumentIdSchema,
103
+ lang: z.number().optional().default(0).describe('Language code (optional). Default is 0 (Chinese). Use 1 for English if available.'),
118
104
  }, async ({ documentId, lang }) => {
119
105
  try {
120
106
  if (!this.feishuService) {
121
107
  return {
122
- content: [
123
- {
124
- type: 'text',
125
- text: 'Feishu service is not initialized. Please check the configuration',
126
- },
127
- ],
108
+ content: [{ type: 'text', text: 'Feishu service is not initialized. Please check the configuration' }],
128
109
  };
129
110
  }
130
- Logger.log(`开始获取飞书文档内容,文档ID: ${documentId},语言: ${lang}`);
111
+ Logger.info(`开始获取飞书文档内容,文档ID: ${documentId},语言: ${lang}`);
131
112
  const content = await this.feishuService.getDocumentContent(documentId, lang);
132
- Logger.log(`飞书文档内容获取成功,内容长度: ${content.length}字符`);
113
+ Logger.info(`飞书文档内容获取成功,内容长度: ${content.length}字符`);
133
114
  return {
134
115
  content: [{ type: 'text', text: content }],
135
116
  };
136
117
  }
137
118
  catch (error) {
138
119
  Logger.error(`获取飞书文档内容失败:`, error);
120
+ const errorMessage = formatErrorMessage(error);
139
121
  return {
140
- content: [{ type: 'text', text: `获取飞书文档内容失败: ${error}` }],
122
+ content: [{ type: 'text', text: `获取飞书文档内容失败: ${errorMessage}` }],
141
123
  };
142
124
  }
143
125
  });
144
126
  // 添加获取飞书文档块工具
145
- this.server.tool('get_feishu_doc_blocks', 'When document structure is needed, obtain the block information about the Feishu document for content analysis or block insertion', {
146
- documentId: z
147
- .string()
148
- .describe('Document ID or URL. Supported formats:\n1. Standard document URL: https://xxx.feishu.cn/docs/xxx or https://xxx.feishu.cn/docx/xxx\n2. API URL: https://open.feishu.cn/open-apis/doc/v2/documents/xxx\n3. Direct document ID (e.g., JcKbdlokYoPIe0xDzJ1cduRXnRf)'),
149
- pageSize: z
150
- .number()
151
- .optional()
152
- .default(500)
153
- .describe('Number of blocks per page. Default is 500'),
127
+ 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.', {
128
+ documentId: DocumentIdSchema,
129
+ 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.'),
154
130
  }, async ({ documentId, pageSize }) => {
155
131
  try {
156
132
  if (!this.feishuService) {
157
133
  return {
158
- content: [
159
- {
160
- type: 'text',
161
- text: 'Feishu service is not initialized. Please check the configuration',
162
- },
163
- ],
134
+ content: [{ type: 'text', text: 'Feishu service is not initialized. Please check the configuration' }],
164
135
  };
165
136
  }
166
- Logger.log(`开始获取飞书文档块,文档ID: ${documentId},页大小: ${pageSize}`);
137
+ Logger.info(`开始获取飞书文档块,文档ID: ${documentId},页大小: ${pageSize}`);
167
138
  const blocks = await this.feishuService.getDocumentBlocks(documentId, pageSize);
168
- Logger.log(`飞书文档块获取成功,共 ${blocks.length} 个块`);
139
+ Logger.info(`飞书文档块获取成功,共 ${blocks.length} 个块`);
169
140
  return {
170
141
  content: [{ type: 'text', text: JSON.stringify(blocks, null, 2) }],
171
142
  };
172
143
  }
173
144
  catch (error) {
174
145
  Logger.error(`获取飞书文档块失败:`, error);
146
+ const errorMessage = formatErrorMessage(error);
175
147
  return {
176
- content: [{ type: 'text', text: `获取飞书文档块失败: ${error}` }],
148
+ content: [{ type: 'text', text: `获取飞书文档块失败: ${errorMessage}` }],
177
149
  };
178
150
  }
179
151
  });
180
- // 添加创建飞书文档块工具
181
- // this.server.tool(
182
- // "create_feishu_text_block",
183
- // "Create a new text block in a Feishu document (AI will automatically convert Markdown syntax to corresponding style attributes: **bold** → bold:true, *italic* → italic:true, ~~strikethrough~~ → strikethrough:true, `code` → inline_code:true)",
184
- // {
185
- // documentId: z.string().describe("Document ID or URL. Supported formats:\n1. Standard document URL: https://xxx.feishu.cn/docs/xxx or https://xxx.feishu.cn/docx/xxx\n2. API URL: https://open.feishu.cn/open-apis/doc/v2/documents/xxx\n3. Direct document ID (e.g., JcKbdlokYoPIe0xDzJ1cduRXnRf)"),
186
- // parentBlockId: z.string().describe("Parent block ID (NOT URL) where the new block will be added as a child. This should be the raw block ID without any URL prefix. When adding blocks at the page level (root level), use the extracted document ID from documentId parameter"),
187
- // textContents: z.array(
188
- // z.object({
189
- // text: z.string().describe("Text content"),
190
- // style: z.object({
191
- // bold: z.boolean().optional().describe("Whether to make text bold. Default is false"),
192
- // italic: z.boolean().optional().describe("Whether to make text italic. Default is false"),
193
- // underline: z.boolean().optional().describe("Whether to add underline. Default is false"),
194
- // strikethrough: z.boolean().optional().describe("Whether to add strikethrough. Default is false"),
195
- // inline_code: z.boolean().optional().describe("Whether to format as inline code. Default is false"),
196
- // text_color: z.number().optional().describe("Text color as a number. Default is 0")
197
- // }).optional().describe("Text style settings")
198
- // })
199
- // ).describe("Array of text content objects. A block can contain multiple text segments with different styles"),
200
- // align: z.number().optional().default(1).describe("Text alignment: 1 for left, 2 for center, 3 for right. Default is 1"),
201
- // index: z.number().optional().default(0).describe("Insertion position index. Default is 0 (insert at the beginning). If unsure about the position, use the get_feishu_doc_blocks tool first to understand the document structure. For consecutive insertions, calculate the next position as previous_index + 1 to avoid querying document structure repeatedly")
202
- // },
203
- // async ({ documentId, parentBlockId, textContents, align = 1, index }) => {
204
- // try {
205
- // if (!this.feishuService) {
206
- // return {
207
- // content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
208
- // };
209
- // }
210
- //
211
- // // 处理Markdown语法转换
212
- // const processedTextContents = textContents.map(content => {
213
- // let { text, style = {} } = content;
214
- //
215
- // // 创建一个新的style对象,避免修改原始对象
216
- // const newStyle = { ...style };
217
- //
218
- // // 处理粗体 **text**
219
- // if (text.match(/\*\*([^*]+)\*\*/g)) {
220
- // text = text.replace(/\*\*([^*]+)\*\*/g, "$1");
221
- // newStyle.bold = true;
222
- // }
223
- //
224
- // // 处理斜体 *text*
225
- // if (text.match(/(?<!\*)\*([^*]+)\*(?!\*)/g)) {
226
- // text = text.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, "$1");
227
- // newStyle.italic = true;
228
- // }
229
- //
230
- // // 处理删除线 ~~text~~
231
- // if (text.match(/~~([^~]+)~~/g)) {
232
- // text = text.replace(/~~([^~]+)~~/g, "$1");
233
- // newStyle.strikethrough = true;
234
- // }
235
- //
236
- // // 处理行内代码 `code`
237
- // if (text.match(/`([^`]+)`/g)) {
238
- // text = text.replace(/`([^`]+)`/g, "$1");
239
- // newStyle.inline_code = true;
240
- // }
241
- //
242
- // return { text, style: newStyle };
243
- // });
244
- //
245
- // Logger.log(`开始创建飞书文本块,文档ID: ${documentId},父块ID: ${parentBlockId},对齐方式: ${align},插入位置: ${index}`);
246
- // const result = await this.feishuService.createTextBlock(documentId, parentBlockId, processedTextContents, align, index);
247
- // Logger.log(`飞书文本块创建成功`);
248
- //
249
- // return {
250
- // content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
251
- // };
252
- // } catch (error) {
253
- // Logger.error(`创建飞书文本块失败:`, error);
254
- // return {
255
- // content: [{ type: "text", text: `创建飞书文本块失败: ${error}` }],
256
- // };
257
- // }
258
- // },
259
- // );
260
- // 添加创建飞书代码块工具
261
- // this.server.tool(
262
- // "create_feishu_code_block",
263
- // "Create a new code block in a Feishu document",
264
- // {
265
- // documentId: z.string().describe("Document ID or URL. Supported formats:\n1. Standard document URL: https://xxx.feishu.cn/docs/xxx or https://xxx.feishu.cn/docx/xxx\n2. API URL: https://open.feishu.cn/open-apis/doc/v2/documents/xxx\n3. Direct document ID (e.g., JcKbdlokYoPIe0xDzJ1cduRXnRf)"),
266
- // parentBlockId: z.string().describe("Parent block ID (NOT URL) where the new block will be added as a child. This should be the raw block ID without any URL prefix. When adding blocks at the page level (root level), use the extracted document ID from documentId parameter"),
267
- // code: z.string().describe("Code content"),
268
- // language: z.number().optional().default(0).describe("Programming language code as a number. Examples: 1: PlainText; 7: Bash; 8: CSharp; 9: C++; 10: C; 12: CSS; 22: Go; 24: HTML; 29: Java; 30: JavaScript; 32: Kotlin; 43: PHP; 49: Python; 52: Ruby; 53: Rust; 56: SQL; 60: Shell; 61: Swift; 63: TypeScript. Default is 0"),
269
- // wrap: z.boolean().optional().default(false).describe("Whether to enable automatic line wrapping. Default is false"),
270
- // index: z.number().optional().default(0).describe("Insertion position index. Default is 0 (insert at the beginning). If unsure about the position, use the get_feishu_doc_blocks tool first to understand the document structure. For consecutive insertions, calculate the next position as previous_index + 1 to avoid querying document structure repeatedly")
271
- // },
272
- // async ({ documentId, parentBlockId, code, language = 0, wrap = false, index = 0 }) => {
273
- // try {
274
- // if (!this.feishuService) {
275
- // return {
276
- // content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
277
- // };
278
- // }
279
- //
280
- // Logger.log(`开始创建飞书代码块,文档ID: ${documentId},父块ID: ${parentBlockId},语言: ${language},自动换行: ${wrap},插入位置: ${index}`);
281
- // const result = await this.feishuService.createCodeBlock(documentId, parentBlockId, code, language, wrap, index);
282
- // Logger.log(`飞书代码块创建成功`);
283
- //
284
- // return {
285
- // content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
286
- // };
287
- // } catch (error) {
288
- // Logger.error(`创建飞书代码块失败:`, error);
289
- // return {
290
- // content: [{ type: "text", text: `创建飞书代码块失败: ${error}` }],
291
- // };
292
- // }
293
- // },
294
- // );
295
- // 添加批量创建飞书块工具
296
- this.server.tool('create_feishu_blocks', 'Create multiple blocks in a Feishu document at once with a single API call', {
297
- documentId: z
298
- .string()
299
- .describe('Document ID or URL. Supported formats:\n1. Standard document URL: https://xxx.feishu.cn/docs/xxx or https://xxx.feishu.cn/docx/xxx\n2. API URL: https://open.feishu.cn/open-apis/doc/v2/documents/xxx\n3. Direct document ID (e.g., JcKbdlokYoPIe0xDzJ1cduRXnRf)'),
300
- parentBlockId: z
301
- .string()
302
- .describe('Parent block ID (NOT URL) where the new blocks will be added as children. This should be the raw block ID without any URL prefix. When adding blocks at the page level (root level), use the extracted document ID from documentId parameter'),
303
- children: z
304
- .array(z.object({
305
- block_type: z
306
- .number()
307
- .describe('Block type number: 2 for text, 3-11 for headings (level+2), 14 for code'),
308
- content: z
309
- .any()
310
- .describe('Block content object that follows Feishu API format'),
311
- }))
312
- .describe('Array of block objects to create in a single API call'),
313
- index: z
314
- .number()
315
- .optional()
316
- .default(0)
317
- .describe('Insertion position index. Default is 0 (insert at the beginning)'),
318
- }, async ({ documentId, parentBlockId, children, index = 0 }) => {
152
+ // 添加获取块内容工具
153
+ 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.', {
154
+ documentId: DocumentIdSchema,
155
+ blockId: BlockIdSchema,
156
+ }, async ({ documentId, blockId }) => {
319
157
  try {
320
158
  if (!this.feishuService) {
321
159
  return {
322
- content: [
323
- {
324
- type: 'text',
325
- text: 'Feishu service is not initialized. Please check the configuration',
326
- },
327
- ],
160
+ content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
328
161
  };
329
162
  }
330
- Logger.log(`开始批量创建飞书块,文档ID: ${documentId},父块ID: ${parentBlockId},块数量: ${children.length},插入位置: ${index}`);
331
- const result = await this.feishuService.createDocumentBlocks(documentId, parentBlockId, children, index);
332
- Logger.log(`飞书块批量创建成功`);
163
+ Logger.info(`开始获取飞书块内容,文档ID: ${documentId},块ID: ${blockId}`);
164
+ const blockContent = await this.feishuService.getBlockContent(documentId, blockId);
165
+ Logger.info(`飞书块内容获取成功,块类型: ${blockContent.block_type}`);
166
+ return {
167
+ content: [{ type: 'text', text: JSON.stringify(blockContent, null, 2) }],
168
+ };
169
+ }
170
+ catch (error) {
171
+ Logger.error(`获取飞书块内容失败:`, error);
172
+ const errorMessage = formatErrorMessage(error);
173
+ return {
174
+ content: [{ type: 'text', text: `获取飞书块内容失败: ${errorMessage}` }],
175
+ };
176
+ }
177
+ });
178
+ // 添加更新块文本内容工具
179
+ 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.', {
180
+ documentId: DocumentIdSchema,
181
+ blockId: BlockIdSchema,
182
+ textElements: TextElementsArraySchema,
183
+ }, async ({ documentId, blockId, textElements }) => {
184
+ try {
185
+ if (!this.feishuService) {
186
+ return {
187
+ content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
188
+ };
189
+ }
190
+ Logger.info(`开始更新飞书块文本内容,文档ID: ${documentId},块ID: ${blockId}`);
191
+ const result = await this.feishuService.updateBlockTextContent(documentId, blockId, textElements);
192
+ Logger.info(`飞书块文本内容更新成功`);
333
193
  return {
334
194
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
335
195
  };
336
196
  }
337
197
  catch (error) {
338
- Logger.error(`批量创建飞书块失败:`, error);
198
+ Logger.error(`更新飞书块文本内容失败:`, error);
199
+ const errorMessage = formatErrorMessage(error);
339
200
  return {
340
- content: [{ type: 'text', text: `批量创建飞书块失败: ${error}` }],
201
+ content: [{ type: 'text', text: `更新飞书块文本内容失败: ${errorMessage}` }],
341
202
  };
342
203
  }
343
204
  });
344
205
  // 添加通用飞书块创建工具(支持文本、代码、标题)
345
- this.server.tool('create_feishu_common_block', 'Create common blocks in a Feishu document (supports text, code, and heading blocks)', {
346
- documentId: z
347
- .string()
348
- .describe('Document ID or URL. Supported formats:\n1. Standard document URL: https://xxx.feishu.cn/docs/xxx or https://xxx.feishu.cn/docx/xxx\n2. API URL: https://open.feishu.cn/open-apis/doc/v2/documents/xxx\n3. Direct document ID (e.g., JcKbdlokYoPIe0xDzJ1cduRXnRf)'),
349
- parentBlockId: z
350
- .string()
351
- .describe('Parent block ID (NOT URL) where the new blocks will be added as children. This should be the raw block ID without any URL prefix. When adding blocks at the page level (root level), use the extracted document ID from documentId parameter'),
352
- startIndex: z
353
- .number()
354
- .optional()
355
- .default(0)
356
- .describe('Starting insertion position index. Default is 0 (insert at the beginning). If individual blocks have their own index specified, those will take precedence'),
357
- blocks: z
358
- .array(z.object({
359
- blockType: z
360
- .enum(['text', 'code', 'heading'])
361
- .describe("Type of block to create: 'text', 'code', or 'heading'"),
362
- options: z
363
- .object({
364
- // 文本块选项 - 当blockType为'text'时使用
365
- text: z
366
- .object({
367
- // 文本内容数组,每个元素包含文本内容和样式
368
- textStyles: z
369
- .array(z.object({
370
- text: z.string().describe('Text segment content'),
371
- style: z
372
- .object({
373
- bold: z
374
- .boolean()
375
- .optional()
376
- .describe('Whether to make text bold. Default is false'),
377
- italic: z
378
- .boolean()
379
- .optional()
380
- .describe('Whether to make text italic. Default is false'),
381
- underline: z
382
- .boolean()
383
- .optional()
384
- .describe('Whether to add underline. Default is false'),
385
- strikethrough: z
386
- .boolean()
387
- .optional()
388
- .describe('Whether to add strikethrough. Default is false'),
389
- inline_code: z
390
- .boolean()
391
- .optional()
392
- .describe('Whether to format as inline code. Default is false'),
393
- text_color: z
394
- .number()
395
- .optional()
396
- .describe('Text color as a number. Default is 0'),
397
- })
398
- .optional()
399
- .describe('Text style settings'),
400
- }))
401
- .optional()
402
- .describe('Array of text content objects with styles. If not provided, content will be used as plain text'),
403
- align: z
404
- .number()
405
- .optional()
406
- .default(1)
407
- .describe('Text alignment: 1 for left, 2 for center, 3 for right. Default is 1'),
408
- })
409
- .optional()
410
- .describe("Text block options. Only used when blockType is 'text'"),
411
- // 代码块选项 - 当blockType为'code'时使用
412
- code: z
413
- .object({
414
- code: z.string().describe('Code content'),
415
- language: z
416
- .number()
417
- .optional()
418
- .default(0)
419
- .describe('Programming language code as a number. Available options:\n1: PlainText, 2: ABAP, 3: Ada, 4: Apache, 5: Apex, 6: Assembly Language, 7: Bash, 8: CSharp, 9: C++, 10: C, 11: COBOL, 12: CSS, 13: CoffeeScript, 14: D, 15: Dart, 16: Delphi, 17: Django, 18: Dockerfile, 19: Erlang, 20: Fortran, 22: Go, 23: Groovy, 24: HTML, 25: HTMLBars, 26: HTTP, 27: Haskell, 28: JSON, 29: Java, 30: JavaScript, 31: Julia, 32: Kotlin, 33: LateX, 34: Lisp, 36: Lua, 37: MATLAB, 38: Makefile, 39: Markdown, 40: Nginx, 41: Objective-C, 43: PHP, 44: Perl, 46: Power Shell, 47: Prolog, 48: ProtoBuf, 49: Python, 50: R, 52: Ruby, 53: Rust, 54: SAS, 55: SCSS, 56: SQL, 57: Scala, 58: Scheme, 60: Shell, 61: Swift, 62: Thrift, 63: TypeScript, 64: VBScript, 65: Visual Basic, 66: XML, 67: YAML, 68: CMake, 69: Diff, 70: Gherkin, 71: GraphQL, 72: OpenGL Shading Language, 73: Properties, 74: Solidity, 75: TOML'),
420
- wrap: z
421
- .boolean()
422
- .optional()
423
- .default(false)
424
- .describe('Whether to enable automatic line wrapping for code blocks. Default is false'),
425
- })
426
- .optional()
427
- .describe("Code block options. Only used when blockType is 'code'"),
428
- // 标题块选项 - 当blockType为'heading'时使用
429
- heading: z
430
- .object({
431
- level: z
432
- .number()
433
- .min(1)
434
- .max(9)
435
- .describe('Heading level from 1 to 9, where 1 is the largest heading (h1) and 9 is the smallest (h9)'),
436
- content: z.string().describe('Heading text content'),
437
- align: z
438
- .number()
439
- .optional()
440
- .default(1)
441
- .describe('Text alignment: 1 for left, 2 for center, 3 for right. Default is 1'),
442
- })
443
- .optional()
444
- .describe("Heading block options. Only used when blockType is 'heading'"),
445
- })
446
- .optional()
447
- .default({}),
448
- }))
449
- .describe('Array of block configurations to create in a single API call'),
206
+ this.server.tool('batch_create_feishu_blocks', 'PREFERRED: Efficiently creates multiple blocks (text, code, heading, list) in a single API call. USE THIS TOOL when creating multiple consecutive blocks at the same position - reduces API calls by up to 90%. KEY FEATURES: (1) Handles any number of blocks by auto-batching large requests (>50 blocks), (2) Creates blocks at consecutive positions in a document. CORRECT FORMAT: mcp_feishu_batch_create_feishu_blocks({documentId:"doc123",parentBlockId:"para123",startIndex:0,blocks:[{blockType:"text",options:{...}},{blockType:"heading",options:{...}}]}). For separate positions, use individual block creation tools instead. For wiki links (https://xxx.feishu.cn/wiki/xxx), first convert with convert_feishu_wiki_to_document_id tool.', {
207
+ documentId: DocumentIdSchema,
208
+ parentBlockId: ParentBlockIdSchema,
209
+ startIndex: StartIndexSchema,
210
+ blocks: z.array(BlockConfigSchema).describe('Array of block configurations. CRITICAL: Must be a JSON array object, NOT a string. CORRECT: blocks:[{...}] - WITHOUT quotes around array. INCORRECT: blocks:"[{...}]". Example: [{blockType:"text",options:{text:{textStyles:[{text:"Hello",style:{bold:true}}]}}},{blockType:"code",options:{code:{code:"console.log()",language:30}}}]. Auto-batches requests when exceeding 50 blocks.'),
450
211
  }, async ({ documentId, parentBlockId, startIndex = 0, blocks }) => {
451
212
  try {
452
213
  if (!this.feishuService) {
@@ -459,127 +220,429 @@ export class FeishuMcpServer {
459
220
  ],
460
221
  };
461
222
  }
462
- Logger.log(`开始批量创建飞书块,文档ID: ${documentId},父块ID: ${parentBlockId},块数量: ${blocks.length},起始插入位置: ${startIndex}`);
463
- // 准备要创建的块内容数组
464
- const blockContents = [];
465
- // 处理每个块配置
466
- for (const blockConfig of blocks) {
467
- const { blockType, options = {} } = blockConfig;
468
- // 使用指定的索引或当前索引
469
- let blockContent;
470
- switch (blockType) {
471
- case 'text':
472
- // 处理文本块
473
- const textOptions = options.text || {
474
- textStyles: [],
475
- align: 1,
476
- };
477
- // 确保textStyles是一个有效的数组
478
- const textStyles = textOptions.textStyles || [];
479
- // 如果textStyles为空,添加一个默认的空文本
480
- if (textStyles.length === 0) {
481
- textStyles.push({
482
- text: '',
483
- style: {}, // 添加空的style对象作为默认值
484
- });
485
- }
486
- const align = textOptions.align || 1;
487
- blockContent = this.feishuService.createTextBlockContent(textStyles, align);
488
- break;
489
- case 'code':
490
- // 处理代码块
491
- const codeOptions = options.code;
492
- if (codeOptions == null) {
493
- break;
494
- }
495
- const codeContent = codeOptions.code || '';
496
- const language = codeOptions.language || 0;
497
- const wrap = codeOptions.wrap || false;
498
- blockContent = this.feishuService.createCodeBlockContent(codeContent, language, wrap);
499
- break;
500
- case 'heading':
501
- // 处理标题块
502
- if (options.heading == null ||
503
- options.heading.content == null) {
504
- break;
223
+ // 类型检查:确保blocks是数组而不是字符串
224
+ if (typeof blocks === 'string') {
225
+ return {
226
+ content: [
227
+ {
228
+ type: 'text',
229
+ text: 'ERROR: The "blocks" parameter was passed as a string instead of an array. Please provide a proper JSON array without quotes. Example: {blocks:[{blockType:"text",options:{...}}]} instead of {blocks:"[{...}]"}',
230
+ },
231
+ ],
232
+ };
233
+ }
234
+ // 如果块数量不超过50,直接调用一次API
235
+ if (blocks.length <= 50) {
236
+ Logger.info(`开始批量创建飞书块,文档ID: ${documentId},父块ID: ${parentBlockId},块数量: ${blocks.length},起始插入位置: ${startIndex}`);
237
+ // 准备要创建的块内容数组
238
+ const blockContents = [];
239
+ // 处理每个块配置
240
+ for (const blockConfig of blocks) {
241
+ const { blockType, options = {} } = blockConfig;
242
+ // 创建块内容
243
+ const blockContent = this.feishuService.createBlockContent(blockType, options);
244
+ if (blockContent) {
245
+ blockContents.push(blockContent);
246
+ Logger.info(`已准备${blockType}块,内容: ${JSON.stringify(blockContent).substring(0, 100)}...`);
247
+ }
248
+ }
249
+ // 批量创建所有块
250
+ const result = await this.feishuService.createDocumentBlocks(documentId, parentBlockId, blockContents, startIndex);
251
+ Logger.info(`飞书块批量创建成功,共创建 ${blockContents.length} 个块`);
252
+ return {
253
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
254
+ };
255
+ }
256
+ else {
257
+ // 如果块数量超过50,需要分批处理
258
+ Logger.info(`块数量(${blocks.length})超过50,将分批创建`);
259
+ const batchSize = 50; // 每批最大50个
260
+ const totalBatches = Math.ceil(blocks.length / batchSize);
261
+ const results = [];
262
+ let currentStartIndex = startIndex;
263
+ let createdBlocksCount = 0;
264
+ let allBatchesSuccess = true;
265
+ // 分批创建块
266
+ for (let batchNum = 0; batchNum < totalBatches; batchNum++) {
267
+ const batchStart = batchNum * batchSize;
268
+ const batchEnd = Math.min((batchNum + 1) * batchSize, blocks.length);
269
+ const currentBatch = blocks.slice(batchStart, batchEnd);
270
+ Logger.info(`处理第 ${batchNum + 1}/${totalBatches} 批,起始位置: ${currentStartIndex},块数量: ${currentBatch.length}`);
271
+ try {
272
+ // 准备当前批次的块内容
273
+ const batchBlockContents = [];
274
+ for (const blockConfig of currentBatch) {
275
+ const { blockType, options = {} } = blockConfig;
276
+ const blockContent = this.feishuService.createBlockContent(blockType, options);
277
+ if (blockContent) {
278
+ batchBlockContents.push(blockContent);
279
+ }
505
280
  }
506
- const headingOptions = options.heading || {};
507
- const headingContent = headingOptions.content || '';
508
- const level = headingOptions.level || 1;
509
- const headingAlign = headingOptions.align || 1;
510
- blockContent = this.feishuService.createHeadingBlockContent(headingContent, level, headingAlign);
511
- break;
281
+ // 批量创建当前批次的块
282
+ const batchResult = await this.feishuService.createDocumentBlocks(documentId, parentBlockId, batchBlockContents, currentStartIndex);
283
+ results.push(batchResult);
284
+ // 计算下一批的起始位置(当前位置+已创建块数量)
285
+ // 注意:每批成功创建后,需要将起始索引更新为当前索引 + 已创建块数量
286
+ createdBlocksCount += batchBlockContents.length;
287
+ currentStartIndex = startIndex + createdBlocksCount;
288
+ Logger.info(`第 ${batchNum + 1}/${totalBatches} 批创建成功,当前已创建 ${createdBlocksCount} 个块`);
289
+ }
290
+ catch (error) {
291
+ Logger.error(`第 ${batchNum + 1}/${totalBatches} 批创建失败:`, error);
292
+ allBatchesSuccess = false;
293
+ // 如果有批次失败,返回详细错误信息
294
+ const errorMessage = formatErrorMessage(error);
295
+ return {
296
+ content: [
297
+ {
298
+ type: 'text',
299
+ text: `批量创建飞书块部分失败:第 ${batchNum + 1}/${totalBatches} 批处理时出错。\n\n` +
300
+ `已成功创建 ${createdBlocksCount} 个块,但还有 ${blocks.length - createdBlocksCount} 个块未能创建。\n\n` +
301
+ `错误信息: ${errorMessage}\n\n` +
302
+ `建议使用 get_feishu_document_blocks 工具获取文档最新状态,确认已创建的内容,然后从索引位置 ${currentStartIndex} 继续创建剩余块。`
303
+ }
304
+ ],
305
+ };
306
+ }
512
307
  }
513
- if (blockContent) {
514
- blockContents.push(blockContent);
515
- Logger.log(`已准备${blockType}块,内容: ${JSON.stringify(blockContent).substring(0, 100)}...`);
308
+ if (allBatchesSuccess) {
309
+ Logger.info(`所有批次创建成功,共创建 ${createdBlocksCount} 个块`);
310
+ return {
311
+ content: [
312
+ {
313
+ type: 'text',
314
+ text: `所有飞书块创建成功,共分 ${totalBatches} 批创建了 ${createdBlocksCount} 个块。\n\n` +
315
+ `最后一批结果: ${JSON.stringify(results[results.length - 1], null, 2)}`
316
+ }
317
+ ],
318
+ };
516
319
  }
517
320
  }
518
- // 批量创建所有块
519
- const result = await this.feishuService.createDocumentBlocks(documentId, parentBlockId, blockContents, startIndex);
520
- Logger.log(`飞书块批量创建成功,共创建 ${blockContents.length} 个块`);
321
+ // 这个return语句是为了避免TypeScript错误,实际上代码永远不会执行到这里
521
322
  return {
522
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
323
+ content: [{ type: 'text', text: '操作完成' }],
523
324
  };
524
325
  }
525
326
  catch (error) {
526
327
  Logger.error(`批量创建飞书块失败:`, error);
328
+ const errorMessage = formatErrorMessage(error);
329
+ return {
330
+ content: [
331
+ {
332
+ type: 'text',
333
+ text: `批量创建飞书块失败: ${errorMessage}\n\n` +
334
+ `建议使用 get_feishu_document_blocks 工具获取文档当前状态,确认是否有部分内容已创建成功。`
335
+ }
336
+ ],
337
+ };
338
+ }
339
+ });
340
+ // 添加创建飞书文本块工具
341
+ 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.", {
342
+ documentId: DocumentIdSchema,
343
+ parentBlockId: ParentBlockIdSchema,
344
+ textContents: TextElementsArraySchema,
345
+ align: AlignSchema,
346
+ index: IndexSchema
347
+ }, async ({ documentId, parentBlockId, textContents, align = 1, index }) => {
348
+ try {
349
+ if (!this.feishuService) {
350
+ return {
351
+ content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
352
+ };
353
+ }
354
+ Logger.info(`开始创建飞书文本块,文档ID: ${documentId},父块ID: ${parentBlockId},对齐方式: ${align},插入位置: ${index}`);
355
+ const result = await this.feishuService.createTextBlock(documentId, parentBlockId, textContents, align, index);
356
+ Logger.info(`飞书文本块创建成功`);
357
+ return {
358
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
359
+ };
360
+ }
361
+ catch (error) {
362
+ Logger.error(`创建飞书文本块失败:`, error);
363
+ const errorMessage = formatErrorMessage(error);
364
+ return {
365
+ content: [{ type: "text", text: `创建飞书文本块失败: ${errorMessage}` }],
366
+ };
367
+ }
368
+ });
369
+ // 添加创建飞书代码块工具
370
+ 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.", {
371
+ documentId: DocumentIdSchema,
372
+ parentBlockId: ParentBlockIdSchema,
373
+ code: z.string().describe("Code content (required). The complete code text to display."),
374
+ language: CodeLanguageSchema,
375
+ wrap: CodeWrapSchema,
376
+ index: IndexSchema
377
+ }, async ({ documentId, parentBlockId, code, language = 1, wrap = false, index = 0 }) => {
378
+ try {
379
+ if (!this.feishuService) {
380
+ return {
381
+ content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
382
+ };
383
+ }
384
+ Logger.info(`开始创建飞书代码块,文档ID: ${documentId},父块ID: ${parentBlockId},语言: ${language},自动换行: ${wrap},插入位置: ${index}`);
385
+ const result = await this.feishuService.createCodeBlock(documentId, parentBlockId, code, language, wrap, index);
386
+ Logger.info(`飞书代码块创建成功`);
387
+ return {
388
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
389
+ };
390
+ }
391
+ catch (error) {
392
+ Logger.error(`创建飞书代码块失败:`, error);
393
+ const errorMessage = formatErrorMessage(error);
394
+ return {
395
+ content: [{ type: "text", text: `创建飞书代码块失败: ${errorMessage}` }],
396
+ };
397
+ }
398
+ });
399
+ // 添加创建飞书标题块工具
400
+ 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.", {
401
+ documentId: DocumentIdSchema,
402
+ parentBlockId: ParentBlockIdSchema,
403
+ 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)."),
404
+ content: z.string().describe("Heading text content (required). The actual text of the heading."),
405
+ align: AlignSchemaWithValidation,
406
+ index: IndexSchema
407
+ }, async ({ documentId, parentBlockId, level, content, align = 1, index = 0 }) => {
408
+ try {
409
+ if (!this.feishuService) {
410
+ return {
411
+ content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
412
+ };
413
+ }
414
+ // 确保align值在合法范围内(1-3)
415
+ if (align !== 1 && align !== 2 && align !== 3) {
416
+ return {
417
+ content: [{ type: "text", text: "错误: 对齐方式(align)参数必须是1(居左)、2(居中)或3(居右)中的一个值。" }],
418
+ };
419
+ }
420
+ Logger.info(`开始创建飞书标题块,文档ID: ${documentId},父块ID: ${parentBlockId},标题级别: ${level},对齐方式: ${align},插入位置: ${index}`);
421
+ const result = await this.feishuService.createHeadingBlock(documentId, parentBlockId, content, level, index, align);
422
+ Logger.info(`飞书标题块创建成功`);
423
+ return {
424
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
425
+ };
426
+ }
427
+ catch (error) {
428
+ Logger.error(`创建飞书标题块失败:`, error);
429
+ const errorMessage = formatErrorMessage(error);
430
+ return {
431
+ content: [{ type: "text", text: `创建飞书标题块失败: ${errorMessage}` }],
432
+ };
433
+ }
434
+ });
435
+ // 添加创建飞书列表块工具
436
+ 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.", {
437
+ documentId: DocumentIdSchema,
438
+ parentBlockId: ParentBlockIdSchema,
439
+ content: z.string().describe("List item content (required). The actual text of the list item."),
440
+ isOrdered: z.boolean().optional().default(false).describe("Whether this is an ordered (numbered) list item. Default is false (bullet point/unordered)."),
441
+ align: AlignSchemaWithValidation,
442
+ index: IndexSchema
443
+ }, async ({ documentId, parentBlockId, content, isOrdered = false, align = 1, index = 0 }) => {
444
+ try {
445
+ if (!this.feishuService) {
446
+ return {
447
+ content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
448
+ };
449
+ }
450
+ // 确保align值在合法范围内(1-3)
451
+ if (align !== 1 && align !== 2 && align !== 3) {
452
+ return {
453
+ content: [{ type: "text", text: "错误: 对齐方式(align)参数必须是1(居左)、2(居中)或3(居右)中的一个值。" }],
454
+ };
455
+ }
456
+ const listType = isOrdered ? "有序" : "无序";
457
+ Logger.info(`开始创建飞书${listType}列表块,文档ID: ${documentId},父块ID: ${parentBlockId},对齐方式: ${align},插入位置: ${index}`);
458
+ const result = await this.feishuService.createListBlock(documentId, parentBlockId, content, isOrdered, index, align);
459
+ Logger.info(`飞书${listType}列表块创建成功`);
460
+ return {
461
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
462
+ };
463
+ }
464
+ catch (error) {
465
+ Logger.error(`创建飞书列表块失败:`, error);
466
+ const errorMessage = formatErrorMessage(error);
467
+ return {
468
+ content: [{ type: "text", text: `创建飞书列表块失败: ${errorMessage}` }],
469
+ };
470
+ }
471
+ });
472
+ // 添加飞书Wiki文档ID转换工具
473
+ 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.', {
474
+ 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'),
475
+ }, async ({ wikiUrl }) => {
476
+ try {
477
+ if (!this.feishuService) {
478
+ return {
479
+ content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
480
+ };
481
+ }
482
+ Logger.info(`开始转换Wiki文档链接,输入: ${wikiUrl}`);
483
+ const documentId = await this.feishuService.convertWikiToDocumentId(wikiUrl);
484
+ Logger.info(`Wiki文档转换成功,可用的文档ID为: ${documentId}`);
485
+ return {
486
+ content: [
487
+ { type: 'text', text: `Converted Wiki link to Document ID: ${documentId}\n\nUse this Document ID with other Feishu document tools.` }
488
+ ],
489
+ };
490
+ }
491
+ catch (error) {
492
+ Logger.error(`转换Wiki文档链接失败:`, error);
493
+ const errorMessage = formatErrorMessage(error);
494
+ return {
495
+ content: [{ type: 'text', text: `转换Wiki文档链接失败: ${errorMessage}` }],
496
+ };
497
+ }
498
+ });
499
+ // 添加删除文档块工具
500
+ this.server.tool('delete_feishu_document_blocks', 'Deletes one or more consecutive blocks from a Feishu document. Use this tool to remove unwanted content, clean up document structure, or clear space before inserting new content. Supports batch deletion for 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.', {
501
+ documentId: DocumentIdSchema,
502
+ parentBlockId: ParentBlockIdSchema,
503
+ startIndex: StartIndexSchema,
504
+ endIndex: EndIndexSchema,
505
+ }, async ({ documentId, parentBlockId, startIndex, endIndex }) => {
506
+ try {
507
+ if (!this.feishuService) {
508
+ return {
509
+ content: [{ type: 'text', text: 'Feishu service is not initialized. Please check the configuration' }],
510
+ };
511
+ }
512
+ Logger.info(`开始删除飞书文档块,文档ID: ${documentId},父块ID: ${parentBlockId},索引范围: ${startIndex}-${endIndex}`);
513
+ const result = await this.feishuService.deleteDocumentBlocks(documentId, parentBlockId, startIndex, endIndex);
514
+ Logger.info(`飞书文档块删除成功,文档修订ID: ${result.document_revision_id}`);
515
+ return {
516
+ content: [{ type: 'text', text: `Successfully deleted blocks from index ${startIndex} to ${endIndex - 1}` }],
517
+ };
518
+ }
519
+ catch (error) {
520
+ Logger.error(`删除飞书文档块失败:`, error);
521
+ const errorMessage = formatErrorMessage(error);
522
+ return {
523
+ content: [{ type: 'text', text: `Failed to delete document blocks: ${errorMessage}` }],
524
+ };
525
+ }
526
+ });
527
+ // 添加获取图片资源工具
528
+ this.server.tool('get_feishu_image_resource', 'Downloads an image resource from Feishu by its media ID. Use this to retrieve images referenced in document blocks or other Feishu resources. Returns the binary image data that can be saved or processed further. For example, extract the media_id from an image block in a document, then use this tool to download the actual image.', {
529
+ mediaId: MediaIdSchema,
530
+ extra: MediaExtraSchema,
531
+ }, async ({ mediaId, extra = '' }) => {
532
+ try {
533
+ if (!this.feishuService) {
534
+ return {
535
+ content: [{ type: 'text', text: 'Feishu service is not initialized. Please check the configuration' }],
536
+ };
537
+ }
538
+ Logger.info(`开始获取飞书图片资源,媒体ID: ${mediaId}`);
539
+ const imageBuffer = await this.feishuService.getImageResource(mediaId, extra);
540
+ Logger.info(`飞书图片资源获取成功,大小: ${imageBuffer.length} 字节`);
541
+ // 将图片数据转为Base64编码,以便在MCP协议中传输
542
+ const base64Image = imageBuffer.toString('base64');
543
+ const mimeType = detectMimeType(imageBuffer);
544
+ return {
545
+ content: [{
546
+ type: 'image',
547
+ mimeType: mimeType,
548
+ data: base64Image
549
+ }],
550
+ };
551
+ }
552
+ catch (error) {
553
+ Logger.error(`获取飞书图片资源失败:`, error);
554
+ const errorMessage = formatErrorMessage(error);
555
+ return {
556
+ content: [{ type: 'text', text: `Failed to get image resource: ${errorMessage}` }],
557
+ };
558
+ }
559
+ });
560
+ // 添加获取根文件夹信息工具
561
+ this.server.tool('get_feishu_root_folder_info', '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.', {}, async () => {
562
+ try {
563
+ if (!this.feishuService) {
564
+ return {
565
+ content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
566
+ };
567
+ }
568
+ Logger.info(`开始获取飞书根文件夹信息`);
569
+ const folderInfo = await this.feishuService.getRootFolderInfo();
570
+ Logger.info(`飞书根文件夹信息获取成功,token: ${folderInfo.token}`);
571
+ return {
572
+ content: [{ type: 'text', text: JSON.stringify(folderInfo, null, 2) }],
573
+ };
574
+ }
575
+ catch (error) {
576
+ Logger.error(`获取飞书根文件夹信息失败:`, error);
577
+ const errorMessage = formatErrorMessage(error, '获取飞书根文件夹信息失败');
578
+ return {
579
+ content: [{ type: 'text', text: errorMessage }],
580
+ };
581
+ }
582
+ });
583
+ // 添加获取文件夹中的文件清单工具
584
+ this.server.tool('get_feishu_folder_files', 'Retrieves a list of files and subfolders in a specified folder. Use this to explore folder contents, view file metadata, and get URLs and tokens for further operations.', {
585
+ folderToken: FolderTokenSchema,
586
+ orderBy: OrderBySchema,
587
+ direction: DirectionSchema
588
+ }, async ({ folderToken, orderBy = 'EditedTime', direction = 'DESC' }) => {
589
+ try {
590
+ if (!this.feishuService) {
591
+ return {
592
+ content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
593
+ };
594
+ }
595
+ Logger.info(`开始获取飞书文件夹中的文件清单,文件夹Token: ${folderToken},排序方式: ${orderBy},排序方向: ${direction}`);
596
+ const fileList = await this.feishuService.getFolderFileList(folderToken, orderBy, direction);
597
+ Logger.info(`飞书文件夹中的文件清单获取成功,共 ${fileList.files?.length || 0} 个文件`);
598
+ return {
599
+ content: [{ type: 'text', text: JSON.stringify(fileList, null, 2) }],
600
+ };
601
+ }
602
+ catch (error) {
603
+ Logger.error(`获取飞书文件夹中的文件清单失败:`, error);
604
+ const errorMessage = formatErrorMessage(error);
605
+ return {
606
+ content: [{ type: 'text', text: `获取飞书文件夹中的文件清单失败: ${errorMessage}` }],
607
+ };
608
+ }
609
+ });
610
+ // 添加创建文件夹工具
611
+ this.server.tool('create_feishu_folder', 'Creates a new folder in a specified parent folder. Use this to organize documents and files within your Feishu Drive structure. Returns the token and URL of the newly created folder.', {
612
+ folderToken: FolderTokenSchema,
613
+ folderName: FolderNameSchema,
614
+ }, async ({ folderToken, folderName }) => {
615
+ try {
616
+ if (!this.feishuService) {
617
+ return {
618
+ content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
619
+ };
620
+ }
621
+ Logger.info(`开始创建飞书文件夹,父文件夹Token: ${folderToken},文件夹名称: ${folderName}`);
622
+ const result = await this.feishuService.createFolder(folderToken, folderName);
623
+ Logger.info(`飞书文件夹创建成功,token: ${result.token},URL: ${result.url}`);
624
+ return {
625
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
626
+ };
627
+ }
628
+ catch (error) {
629
+ Logger.error(`创建飞书文件夹失败:`, error);
630
+ const errorMessage = formatErrorMessage(error);
527
631
  return {
528
- content: [{ type: 'text', text: `批量创建飞书块失败: ${error}` }],
632
+ content: [{ type: 'text', text: `创建飞书文件夹失败: ${errorMessage}` }],
529
633
  };
530
634
  }
531
635
  });
532
- // // 添加创建飞书标题块工具
533
- // this.server.tool(
534
- // "create_feishu_heading_block",
535
- // "Create a heading block in a Feishu document with specified level (1-9)",
536
- // {
537
- // documentId: z.string().describe("Document ID or URL. Supported formats:\n1. Standard document URL: https://xxx.feishu.cn/docs/xxx or https://xxx.feishu.cn/docx/xxx\n2. API URL: https://open.feishu.cn/open-apis/doc/v2/documents/xxx\n3. Direct document ID (e.g., JcKbdlokYoPIe0xDzJ1cduRXnRf)"),
538
- // parentBlockId: z.string().describe("Parent block ID (NOT URL) where the new block will be added as a child. This should be the raw block ID without any URL prefix. When adding blocks at the page level (root level), use the extracted document ID from documentId parameter"),
539
- // level: z.number().min(1).max(9).describe("Heading level from 1 to 9, where 1 is the largest heading (h1) and 9 is the smallest (h9)"),
540
- // content: z.string().describe("Heading text content"),
541
- // index: z.number().optional().default(0).describe("Insertion position index. Default is 0 (insert at the beginning). If unsure about the position, use the get_feishu_doc_blocks tool first to understand the document structure. For consecutive insertions, calculate the next position as previous_index + 1 to avoid querying document structure repeatedly")
542
- // },
543
- // async ({ documentId, parentBlockId, level, content, index = 0 }) => {
544
- // try {
545
- // if (!this.feishuService) {
546
- // return {
547
- // content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
548
- // };
549
- // }
550
- //
551
- // Logger.log(`开始创建飞书标题块,文档ID: ${documentId},父块ID: ${parentBlockId},标题级别: ${level},插入位置: ${index}`);
552
- // const result = await this.feishuService.createHeadingBlock(documentId, parentBlockId, content, level, index);
553
- // Logger.log(`飞书标题块创建成功`);
554
- //
555
- // return {
556
- // content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
557
- // };
558
- // } catch (error) {
559
- // Logger.error(`创建飞书标题块失败:`, error);
560
- // return {
561
- // content: [{ type: "text", text: `创建飞书标题块失败: ${error}` }],
562
- // };
563
- // }
564
- // },
565
- // );
566
636
  }
567
637
  async connect(transport) {
568
- // Logger.log("Connecting to transport...");
569
638
  await this.server.connect(transport);
570
- Logger.log = (...args) => {
571
- this.server.server.sendLoggingMessage({
572
- level: 'info',
573
- data: args,
574
- });
639
+ Logger.info = (...args) => {
640
+ this.server.server.sendLoggingMessage({ level: 'info', data: args });
575
641
  };
576
642
  Logger.error = (...args) => {
577
- this.server.server.sendLoggingMessage({
578
- level: 'error',
579
- data: args,
580
- });
643
+ this.server.server.sendLoggingMessage({ level: 'error', data: args });
581
644
  };
582
- Logger.log('Server connected and ready to process requests');
645
+ Logger.info('Server connected and ready to process requests');
583
646
  }
584
647
  async startHttpServer(port) {
585
648
  const app = express();
@@ -595,12 +658,12 @@ export class FeishuMcpServer {
595
658
  }
596
659
  await this.sseTransport.handlePostMessage(req, res);
597
660
  });
598
- Logger.log = console.log;
661
+ Logger.info = console.log;
599
662
  Logger.error = console.error;
600
663
  app.listen(port, () => {
601
- Logger.log(`HTTP server listening on port ${port}`);
602
- Logger.log(`SSE endpoint available at http://localhost:${port}/sse`);
603
- Logger.log(`Message endpoint available at http://localhost:${port}/messages`);
664
+ Logger.info(`HTTP server listening on port ${port}`);
665
+ Logger.info(`SSE endpoint available at http://localhost:${port}/sse`);
666
+ Logger.info(`Message endpoint available at http://localhost:${port}/messages`);
604
667
  });
605
668
  }
606
669
  }