feishu-mcp 0.0.10 → 0.0.11
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/manager/sseConnectionManager.js +104 -0
- package/dist/mcp/feishuMcp.js +67 -0
- package/dist/mcp/tools/feishuBlockTools.js +427 -0
- package/dist/mcp/tools/feishuFolderTools.js +86 -0
- package/dist/mcp/tools/feishuTools.js +137 -0
- package/dist/server.js +39 -637
- package/dist/services/blockFactory.js +29 -11
- package/dist/services/feishuApiService.js +90 -3
- package/dist/types/feishuSchema.js +8 -7
- package/dist/utils/logger.js +27 -11
- package/package.json +1 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Logger } from '../utils/logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* SSE连接管理器 - 负责管理所有的SSE长连接和心跳机制
|
|
4
|
+
*/
|
|
5
|
+
export class SSEConnectionManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
Object.defineProperty(this, "transports", {
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
writable: true,
|
|
11
|
+
value: {}
|
|
12
|
+
});
|
|
13
|
+
Object.defineProperty(this, "connections", {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
configurable: true,
|
|
16
|
+
writable: true,
|
|
17
|
+
value: new Map()
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(this, "keepAliveIntervalId", {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: null
|
|
24
|
+
});
|
|
25
|
+
Object.defineProperty(this, "KEEP_ALIVE_INTERVAL_MS", {
|
|
26
|
+
enumerable: true,
|
|
27
|
+
configurable: true,
|
|
28
|
+
writable: true,
|
|
29
|
+
value: 1000 * 25
|
|
30
|
+
}); // 25秒心跳间隔
|
|
31
|
+
this.startGlobalKeepAlive();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 启动全局心跳管理
|
|
35
|
+
*/
|
|
36
|
+
startGlobalKeepAlive() {
|
|
37
|
+
if (this.keepAliveIntervalId) {
|
|
38
|
+
clearInterval(this.keepAliveIntervalId);
|
|
39
|
+
}
|
|
40
|
+
this.keepAliveIntervalId = setInterval(() => {
|
|
41
|
+
Logger.info(`[KeepAlive] Sending keepalive to ${this.connections.size} connections`);
|
|
42
|
+
for (const [sessionId, connection] of this.connections.entries()) {
|
|
43
|
+
if (!connection.res.writableEnded) {
|
|
44
|
+
connection.res.write(': keepalive\n\n');
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// 移除已关闭的连接
|
|
48
|
+
this.removeConnection(sessionId);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}, this.KEEP_ALIVE_INTERVAL_MS);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 添加新的SSE连接
|
|
55
|
+
*/
|
|
56
|
+
addConnection(sessionId, transport, req, res) {
|
|
57
|
+
this.transports[sessionId] = transport;
|
|
58
|
+
this.connections.set(sessionId, { res });
|
|
59
|
+
console.info(`[SSE Connection] Client connected: ${sessionId}`);
|
|
60
|
+
req.on('close', () => {
|
|
61
|
+
this.removeConnection(sessionId);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 移除SSE连接
|
|
66
|
+
*/
|
|
67
|
+
removeConnection(sessionId) {
|
|
68
|
+
const transport = this.transports[sessionId];
|
|
69
|
+
if (transport) {
|
|
70
|
+
try {
|
|
71
|
+
transport.close();
|
|
72
|
+
Logger.info(`[SSE Connection] Transport closed for: ${sessionId}`);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
Logger.error(`[SSE Connection] Error closing transport for: ${sessionId}`, error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
delete this.transports[sessionId];
|
|
79
|
+
this.connections.delete(sessionId);
|
|
80
|
+
console.info(`[SSE Connection] Client disconnected: ${sessionId}`);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 获取指定sessionId的传输实例
|
|
84
|
+
*/
|
|
85
|
+
getTransport(sessionId) {
|
|
86
|
+
console.info(`[SSE Connection] Getting transport for sessionId: ${sessionId}`);
|
|
87
|
+
return this.transports[sessionId];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 关闭连接管理器
|
|
91
|
+
*/
|
|
92
|
+
shutdown() {
|
|
93
|
+
if (this.keepAliveIntervalId) {
|
|
94
|
+
clearInterval(this.keepAliveIntervalId);
|
|
95
|
+
this.keepAliveIntervalId = null;
|
|
96
|
+
}
|
|
97
|
+
// 关闭所有连接
|
|
98
|
+
Logger.info(`[SSE Connection] Shutting down all connections (${this.connections.size} active)`);
|
|
99
|
+
for (const sessionId of this.connections.keys()) {
|
|
100
|
+
this.removeConnection(sessionId);
|
|
101
|
+
}
|
|
102
|
+
Logger.info(`[SSE Connection] All connections closed`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { FeishuApiService } from '../services/feishuApiService.js';
|
|
3
|
+
import { Logger } from '../utils/logger.js';
|
|
4
|
+
import { registerFeishuTools } from './tools/feishuTools.js';
|
|
5
|
+
import { registerFeishuBlockTools } from './tools/feishuBlockTools.js';
|
|
6
|
+
import { registerFeishuFolderTools } from './tools/feishuFolderTools.js';
|
|
7
|
+
const serverInfo = {
|
|
8
|
+
name: "Feishu MCP Server",
|
|
9
|
+
version: "0.0.9",
|
|
10
|
+
};
|
|
11
|
+
const serverOptions = {
|
|
12
|
+
capabilities: { logging: {}, tools: {} },
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* 飞书MCP服务类
|
|
16
|
+
* 继承自McpServer,提供飞书工具注册和初始化功能
|
|
17
|
+
*/
|
|
18
|
+
export class FeishuMcp extends McpServer {
|
|
19
|
+
/**
|
|
20
|
+
* 构造函数
|
|
21
|
+
*/
|
|
22
|
+
constructor() {
|
|
23
|
+
super(serverInfo, serverOptions);
|
|
24
|
+
Object.defineProperty(this, "feishuService", {
|
|
25
|
+
enumerable: true,
|
|
26
|
+
configurable: true,
|
|
27
|
+
writable: true,
|
|
28
|
+
value: null
|
|
29
|
+
});
|
|
30
|
+
// 初始化飞书服务
|
|
31
|
+
this.initFeishuService();
|
|
32
|
+
// 注册所有工具
|
|
33
|
+
if (this.feishuService) {
|
|
34
|
+
this.registerAllTools();
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
Logger.error('无法注册飞书工具: 飞书服务初始化失败');
|
|
38
|
+
throw new Error('飞书服务初始化失败');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 初始化飞书API服务
|
|
43
|
+
*/
|
|
44
|
+
initFeishuService() {
|
|
45
|
+
try {
|
|
46
|
+
// 使用单例模式获取飞书服务实例
|
|
47
|
+
this.feishuService = FeishuApiService.getInstance();
|
|
48
|
+
Logger.info('飞书服务初始化成功');
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
Logger.error('飞书服务初始化失败:', error);
|
|
52
|
+
this.feishuService = null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 注册所有飞书MCP工具
|
|
57
|
+
*/
|
|
58
|
+
registerAllTools() {
|
|
59
|
+
if (!this.feishuService) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// 注册所有工具
|
|
63
|
+
registerFeishuTools(this, this.feishuService);
|
|
64
|
+
registerFeishuBlockTools(this, this.feishuService);
|
|
65
|
+
registerFeishuFolderTools(this, this.feishuService);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { formatErrorMessage } from '../../utils/error.js';
|
|
3
|
+
import { Logger } from '../../utils/logger.js';
|
|
4
|
+
import { detectMimeType } from '../../utils/document.js';
|
|
5
|
+
import { DocumentIdSchema, ParentBlockIdSchema, BlockIdSchema, IndexSchema, StartIndexSchema, EndIndexSchema, AlignSchema, AlignSchemaWithValidation, TextElementsArraySchema, CodeLanguageSchema, CodeWrapSchema, BlockConfigSchema, MediaIdSchema, MediaExtraSchema } from '../../types/feishuSchema.js';
|
|
6
|
+
/**
|
|
7
|
+
* 注册飞书块相关的MCP工具
|
|
8
|
+
* @param server MCP服务器实例
|
|
9
|
+
* @param feishuService 飞书API服务实例
|
|
10
|
+
*/
|
|
11
|
+
export function registerFeishuBlockTools(server, feishuService) {
|
|
12
|
+
// 添加更新块文本内容工具
|
|
13
|
+
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.', {
|
|
14
|
+
documentId: DocumentIdSchema,
|
|
15
|
+
blockId: BlockIdSchema,
|
|
16
|
+
textElements: TextElementsArraySchema,
|
|
17
|
+
}, async ({ documentId, blockId, textElements }) => {
|
|
18
|
+
try {
|
|
19
|
+
if (!feishuService) {
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
Logger.info(`开始更新飞书块文本内容,文档ID: ${documentId},块ID: ${blockId}`);
|
|
25
|
+
const result = await feishuService.updateBlockTextContent(documentId, blockId, textElements);
|
|
26
|
+
Logger.info(`飞书块文本内容更新成功`);
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
Logger.error(`更新飞书块文本内容失败:`, error);
|
|
33
|
+
const errorMessage = formatErrorMessage(error);
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: 'text', text: `更新飞书块文本内容失败: ${errorMessage}` }],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
// 添加通用飞书块创建工具(支持文本、代码、标题)
|
|
40
|
+
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, (3) Supports direct heading level format (e.g. "heading1", "heading2") or standard "heading" type with level in options. CORRECT FORMAT: mcp_feishu_batch_create_feishu_blocks({documentId:"doc123",parentBlockId:"para123",startIndex:0,blocks:[{blockType:"text",options:{...}},{blockType:"heading1",options:{heading:{content:"Title"}}}]}). 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.', {
|
|
41
|
+
documentId: DocumentIdSchema,
|
|
42
|
+
parentBlockId: ParentBlockIdSchema,
|
|
43
|
+
index: IndexSchema,
|
|
44
|
+
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:"heading1",options:{heading:{content:"My Title"}}}]. Auto-batches requests when exceeding 50 blocks.'),
|
|
45
|
+
}, async ({ documentId, parentBlockId, index = 0, blocks }) => {
|
|
46
|
+
try {
|
|
47
|
+
if (!feishuService) {
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: 'text',
|
|
52
|
+
text: 'Feishu service is not initialized. Please check the configuration',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// 类型检查:确保blocks是数组而不是字符串
|
|
58
|
+
if (typeof blocks === 'string') {
|
|
59
|
+
return {
|
|
60
|
+
content: [
|
|
61
|
+
{
|
|
62
|
+
type: 'text',
|
|
63
|
+
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:"[{...}]"}',
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// 如果块数量不超过50,直接调用一次API
|
|
69
|
+
if (blocks.length <= 50) {
|
|
70
|
+
Logger.info(`开始批量创建飞书块,文档ID: ${documentId},父块ID: ${parentBlockId},块数量: ${blocks.length},起始插入位置: ${index}`);
|
|
71
|
+
// 准备要创建的块内容数组
|
|
72
|
+
const blockContents = [];
|
|
73
|
+
// 处理每个块配置
|
|
74
|
+
for (const blockConfig of blocks) {
|
|
75
|
+
const { blockType, options = {} } = blockConfig;
|
|
76
|
+
// 创建块内容
|
|
77
|
+
try {
|
|
78
|
+
const blockContent = feishuService.createBlockContent(blockType, options);
|
|
79
|
+
if (blockContent) {
|
|
80
|
+
blockContents.push(blockContent);
|
|
81
|
+
Logger.info(`已准备${blockType}块,内容: ${JSON.stringify(blockContent).substring(0, 100)}...`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
Logger.warn(`创建${blockType}块失败,跳过此块`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
Logger.error(`处理块类型${blockType}时出错: ${error}`);
|
|
89
|
+
return {
|
|
90
|
+
content: [{
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: `处理块类型"${blockType}"时出错: ${error}\n请检查该块类型的配置是否正确。`
|
|
93
|
+
}],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// 批量创建所有块
|
|
98
|
+
const result = await feishuService.createDocumentBlocks(documentId, parentBlockId, blockContents, index);
|
|
99
|
+
Logger.info(`飞书块批量创建成功,共创建 ${blockContents.length} 个块`);
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
102
|
+
...result,
|
|
103
|
+
nextIndex: index + blockContents.length,
|
|
104
|
+
totalBlocksCreated: blockContents.length
|
|
105
|
+
}, null, 2) }],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// 如果块数量超过50,需要分批处理
|
|
110
|
+
Logger.info(`块数量(${blocks.length})超过50,将分批创建`);
|
|
111
|
+
const batchSize = 50; // 每批最大50个
|
|
112
|
+
const totalBatches = Math.ceil(blocks.length / batchSize);
|
|
113
|
+
const results = [];
|
|
114
|
+
let currentStartIndex = index;
|
|
115
|
+
let createdBlocksCount = 0;
|
|
116
|
+
let allBatchesSuccess = true;
|
|
117
|
+
// 分批创建块
|
|
118
|
+
for (let batchNum = 0; batchNum < totalBatches; batchNum++) {
|
|
119
|
+
const batchStart = batchNum * batchSize;
|
|
120
|
+
const batchEnd = Math.min((batchNum + 1) * batchSize, blocks.length);
|
|
121
|
+
const currentBatch = blocks.slice(batchStart, batchEnd);
|
|
122
|
+
Logger.info(`处理第 ${batchNum + 1}/${totalBatches} 批,起始位置: ${currentStartIndex},块数量: ${currentBatch.length}`);
|
|
123
|
+
try {
|
|
124
|
+
// 准备当前批次的块内容
|
|
125
|
+
const batchBlockContents = [];
|
|
126
|
+
for (const blockConfig of currentBatch) {
|
|
127
|
+
const { blockType, options = {} } = blockConfig;
|
|
128
|
+
try {
|
|
129
|
+
const blockContent = feishuService.createBlockContent(blockType, options);
|
|
130
|
+
if (blockContent) {
|
|
131
|
+
batchBlockContents.push(blockContent);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
Logger.warn(`创建${blockType}块失败,跳过此块`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
Logger.error(`处理块类型${blockType}时出错: ${error}`);
|
|
139
|
+
return {
|
|
140
|
+
content: [{
|
|
141
|
+
type: 'text',
|
|
142
|
+
text: `处理块类型"${blockType}"时出错: ${error}\n请检查该块类型的配置是否正确。`
|
|
143
|
+
}],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// 批量创建当前批次的块
|
|
148
|
+
const batchResult = await feishuService.createDocumentBlocks(documentId, parentBlockId, batchBlockContents, currentStartIndex);
|
|
149
|
+
results.push(batchResult);
|
|
150
|
+
// 计算下一批的起始位置(当前位置+已创建块数量)
|
|
151
|
+
// 注意:每批成功创建后,需要将起始索引更新为当前索引 + 已创建块数量
|
|
152
|
+
createdBlocksCount += batchBlockContents.length;
|
|
153
|
+
currentStartIndex = index + createdBlocksCount;
|
|
154
|
+
Logger.info(`第 ${batchNum + 1}/${totalBatches} 批创建成功,当前已创建 ${createdBlocksCount} 个块`);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
Logger.error(`第 ${batchNum + 1}/${totalBatches} 批创建失败:`, error);
|
|
158
|
+
allBatchesSuccess = false;
|
|
159
|
+
// 如果有批次失败,返回详细错误信息
|
|
160
|
+
const errorMessage = formatErrorMessage(error);
|
|
161
|
+
return {
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: 'text',
|
|
165
|
+
text: `批量创建飞书块部分失败:第 ${batchNum + 1}/${totalBatches} 批处理时出错。\n\n` +
|
|
166
|
+
`已成功创建 ${createdBlocksCount} 个块,但还有 ${blocks.length - createdBlocksCount} 个块未能创建。\n\n` +
|
|
167
|
+
`错误信息: ${errorMessage}\n\n` +
|
|
168
|
+
`建议使用 get_feishu_document_blocks 工具获取文档最新状态,确认已创建的内容,然后从索引位置 ${currentStartIndex} 继续创建剩余块。`
|
|
169
|
+
}
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (allBatchesSuccess) {
|
|
175
|
+
Logger.info(`所有批次创建成功,共创建 ${createdBlocksCount} 个块`);
|
|
176
|
+
return {
|
|
177
|
+
content: [
|
|
178
|
+
{
|
|
179
|
+
type: 'text',
|
|
180
|
+
text: `所有飞书块创建成功,共分 ${totalBatches} 批创建了 ${createdBlocksCount} 个块。\n\n` +
|
|
181
|
+
`最后一批结果: ${JSON.stringify(results[results.length - 1], null, 2)}\n\n` +
|
|
182
|
+
`下一个索引位置: ${currentStartIndex},总创建块数: ${createdBlocksCount}`
|
|
183
|
+
}
|
|
184
|
+
],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// 这个return语句是为了避免TypeScript错误,实际上代码永远不会执行到这里
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: 'text', text: '操作完成' }],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
Logger.error(`批量创建飞书块失败:`, error);
|
|
195
|
+
const errorMessage = formatErrorMessage(error);
|
|
196
|
+
return {
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: 'text',
|
|
200
|
+
text: `批量创建飞书块失败: ${errorMessage}\n\n` +
|
|
201
|
+
`建议使用 get_feishu_document_blocks 工具获取文档当前状态,确认是否有部分内容已创建成功。`
|
|
202
|
+
}
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
// 添加创建飞书文本块工具
|
|
208
|
+
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.", {
|
|
209
|
+
documentId: DocumentIdSchema,
|
|
210
|
+
parentBlockId: ParentBlockIdSchema,
|
|
211
|
+
textContents: TextElementsArraySchema,
|
|
212
|
+
align: AlignSchema,
|
|
213
|
+
index: IndexSchema
|
|
214
|
+
}, async ({ documentId, parentBlockId, textContents, align = 1, index }) => {
|
|
215
|
+
try {
|
|
216
|
+
if (!feishuService) {
|
|
217
|
+
return {
|
|
218
|
+
content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
Logger.info(`开始创建飞书文本块,文档ID: ${documentId},父块ID: ${parentBlockId},对齐方式: ${align},插入位置: ${index}`);
|
|
222
|
+
const result = await feishuService.createTextBlock(documentId, parentBlockId, textContents, align, index);
|
|
223
|
+
Logger.info(`飞书文本块创建成功`);
|
|
224
|
+
return {
|
|
225
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
Logger.error(`创建飞书文本块失败:`, error);
|
|
230
|
+
const errorMessage = formatErrorMessage(error);
|
|
231
|
+
return {
|
|
232
|
+
content: [{ type: "text", text: `创建飞书文本块失败: ${errorMessage}` }],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
// 添加创建飞书代码块工具
|
|
237
|
+
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.", {
|
|
238
|
+
documentId: DocumentIdSchema,
|
|
239
|
+
parentBlockId: ParentBlockIdSchema,
|
|
240
|
+
code: z.string().describe("Code content (required). The complete code text to display."),
|
|
241
|
+
language: CodeLanguageSchema,
|
|
242
|
+
wrap: CodeWrapSchema,
|
|
243
|
+
index: IndexSchema
|
|
244
|
+
}, async ({ documentId, parentBlockId, code, language = 1, wrap = false, index = 0 }) => {
|
|
245
|
+
try {
|
|
246
|
+
if (!feishuService) {
|
|
247
|
+
return {
|
|
248
|
+
content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
Logger.info(`开始创建飞书代码块,文档ID: ${documentId},父块ID: ${parentBlockId},语言: ${language},自动换行: ${wrap},插入位置: ${index}`);
|
|
252
|
+
const result = await feishuService.createCodeBlock(documentId, parentBlockId, code, language, wrap, index);
|
|
253
|
+
Logger.info(`飞书代码块创建成功`);
|
|
254
|
+
return {
|
|
255
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
Logger.error(`创建飞书代码块失败:`, error);
|
|
260
|
+
const errorMessage = formatErrorMessage(error);
|
|
261
|
+
return {
|
|
262
|
+
content: [{ type: "text", text: `创建飞书代码块失败: ${errorMessage}` }],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
// 添加创建飞书标题块工具
|
|
267
|
+
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.", {
|
|
268
|
+
documentId: DocumentIdSchema,
|
|
269
|
+
parentBlockId: ParentBlockIdSchema,
|
|
270
|
+
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)."),
|
|
271
|
+
content: z.string().describe("Heading text content (required). The actual text of the heading."),
|
|
272
|
+
align: AlignSchemaWithValidation,
|
|
273
|
+
index: IndexSchema
|
|
274
|
+
}, async ({ documentId, parentBlockId, level, content, align = 1, index = 0 }) => {
|
|
275
|
+
try {
|
|
276
|
+
if (!feishuService) {
|
|
277
|
+
return {
|
|
278
|
+
content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// 确保align值在合法范围内(1-3)
|
|
282
|
+
if (align !== 1 && align !== 2 && align !== 3) {
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: "text", text: "错误: 对齐方式(align)参数必须是1(居左)、2(居中)或3(居右)中的一个值。" }],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
Logger.info(`开始创建飞书标题块,文档ID: ${documentId},父块ID: ${parentBlockId},标题级别: ${level},对齐方式: ${align},插入位置: ${index}`);
|
|
288
|
+
const result = await feishuService.createHeadingBlock(documentId, parentBlockId, content, level, index, align);
|
|
289
|
+
Logger.info(`飞书标题块创建成功`);
|
|
290
|
+
return {
|
|
291
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
Logger.error(`创建飞书标题块失败:`, error);
|
|
296
|
+
const errorMessage = formatErrorMessage(error);
|
|
297
|
+
return {
|
|
298
|
+
content: [{ type: "text", text: `创建飞书标题块失败: ${errorMessage}` }],
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
// 添加创建飞书列表块工具
|
|
303
|
+
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.", {
|
|
304
|
+
documentId: DocumentIdSchema,
|
|
305
|
+
parentBlockId: ParentBlockIdSchema,
|
|
306
|
+
content: z.string().describe("List item content (required). The actual text of the list item."),
|
|
307
|
+
isOrdered: z.boolean().optional().default(false).describe("Whether this is an ordered (numbered) list item. Default is false (bullet point/unordered)."),
|
|
308
|
+
align: AlignSchemaWithValidation,
|
|
309
|
+
index: IndexSchema
|
|
310
|
+
}, async ({ documentId, parentBlockId, content, isOrdered = false, align = 1, index = 0 }) => {
|
|
311
|
+
try {
|
|
312
|
+
if (!feishuService) {
|
|
313
|
+
return {
|
|
314
|
+
content: [{ type: "text", text: "Feishu service is not initialized. Please check the configuration" }],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
// 确保align值在合法范围内(1-3)
|
|
318
|
+
if (align !== 1 && align !== 2 && align !== 3) {
|
|
319
|
+
return {
|
|
320
|
+
content: [{ type: "text", text: "错误: 对齐方式(align)参数必须是1(居左)、2(居中)或3(居右)中的一个值。" }],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
const listType = isOrdered ? "有序" : "无序";
|
|
324
|
+
Logger.info(`开始创建飞书${listType}列表块,文档ID: ${documentId},父块ID: ${parentBlockId},对齐方式: ${align},插入位置: ${index}`);
|
|
325
|
+
const result = await feishuService.createListBlock(documentId, parentBlockId, content, isOrdered, index, align);
|
|
326
|
+
Logger.info(`飞书${listType}列表块创建成功`);
|
|
327
|
+
return {
|
|
328
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
Logger.error(`创建飞书列表块失败:`, error);
|
|
333
|
+
const errorMessage = formatErrorMessage(error);
|
|
334
|
+
return {
|
|
335
|
+
content: [{ type: "text", text: `创建飞书列表块失败: ${errorMessage}` }],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
// 添加飞书Wiki文档ID转换工具
|
|
340
|
+
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.', {
|
|
341
|
+
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'),
|
|
342
|
+
}, async ({ wikiUrl }) => {
|
|
343
|
+
try {
|
|
344
|
+
if (!feishuService) {
|
|
345
|
+
return {
|
|
346
|
+
content: [{ type: 'text', text: '飞书服务未初始化,请检查配置' }],
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
Logger.info(`开始转换Wiki文档链接,输入: ${wikiUrl}`);
|
|
350
|
+
const documentId = await feishuService.convertWikiToDocumentId(wikiUrl);
|
|
351
|
+
Logger.info(`Wiki文档转换成功,可用的文档ID为: ${documentId}`);
|
|
352
|
+
return {
|
|
353
|
+
content: [
|
|
354
|
+
{ type: 'text', text: `Converted Wiki link to Document ID: ${documentId}\n\nUse this Document ID with other Feishu document tools.` }
|
|
355
|
+
],
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
Logger.error(`转换Wiki文档链接失败:`, error);
|
|
360
|
+
const errorMessage = formatErrorMessage(error);
|
|
361
|
+
return {
|
|
362
|
+
content: [{ type: 'text', text: `转换Wiki文档链接失败: ${errorMessage}` }],
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
// 添加删除文档块工具
|
|
367
|
+
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.', {
|
|
368
|
+
documentId: DocumentIdSchema,
|
|
369
|
+
parentBlockId: ParentBlockIdSchema,
|
|
370
|
+
startIndex: StartIndexSchema,
|
|
371
|
+
endIndex: EndIndexSchema,
|
|
372
|
+
}, async ({ documentId, parentBlockId, startIndex, endIndex }) => {
|
|
373
|
+
try {
|
|
374
|
+
if (!feishuService) {
|
|
375
|
+
return {
|
|
376
|
+
content: [{ type: 'text', text: 'Feishu service is not initialized. Please check the configuration' }],
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
Logger.info(`开始删除飞书文档块,文档ID: ${documentId},父块ID: ${parentBlockId},索引范围: ${startIndex}-${endIndex}`);
|
|
380
|
+
const result = await feishuService.deleteDocumentBlocks(documentId, parentBlockId, startIndex, endIndex);
|
|
381
|
+
Logger.info(`飞书文档块删除成功,文档修订ID: ${result.document_revision_id}`);
|
|
382
|
+
return {
|
|
383
|
+
content: [{ type: 'text', text: `Successfully deleted blocks from index ${startIndex} to ${endIndex - 1}` }],
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
Logger.error(`删除飞书文档块失败:`, error);
|
|
388
|
+
const errorMessage = formatErrorMessage(error);
|
|
389
|
+
return {
|
|
390
|
+
content: [{ type: 'text', text: `Failed to delete document blocks: ${errorMessage}` }],
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
// 添加获取图片资源工具
|
|
395
|
+
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.', {
|
|
396
|
+
mediaId: MediaIdSchema,
|
|
397
|
+
extra: MediaExtraSchema,
|
|
398
|
+
}, async ({ mediaId, extra = '' }) => {
|
|
399
|
+
try {
|
|
400
|
+
if (!feishuService) {
|
|
401
|
+
return {
|
|
402
|
+
content: [{ type: 'text', text: 'Feishu service is not initialized. Please check the configuration' }],
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
Logger.info(`开始获取飞书图片资源,媒体ID: ${mediaId}`);
|
|
406
|
+
const imageBuffer = await feishuService.getImageResource(mediaId, extra);
|
|
407
|
+
Logger.info(`飞书图片资源获取成功,大小: ${imageBuffer.length} 字节`);
|
|
408
|
+
// 将图片数据转为Base64编码,以便在MCP协议中传输
|
|
409
|
+
const base64Image = imageBuffer.toString('base64');
|
|
410
|
+
const mimeType = detectMimeType(imageBuffer);
|
|
411
|
+
return {
|
|
412
|
+
content: [{
|
|
413
|
+
type: 'image',
|
|
414
|
+
mimeType: mimeType,
|
|
415
|
+
data: base64Image
|
|
416
|
+
}],
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
Logger.error(`获取飞书图片资源失败:`, error);
|
|
421
|
+
const errorMessage = formatErrorMessage(error);
|
|
422
|
+
return {
|
|
423
|
+
content: [{ type: 'text', text: `Failed to get image resource: ${errorMessage}` }],
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
}
|