Skip to main content
Glama

Feishu MCP Server

index.ts7.2 kB
import { z } from 'zod'; import { convertDescriptionToString, McpToolDescription } from '../types'; import { addMermaidBlockMarkers } from '../../utils/markdown-processor'; // 类型定义 interface MediaUploadParams { url?: string; text?: string; base64?: string; svg2png?: string; file_name: string; parent_type?: 'ccm_import_open' | 'docx_image' | 'docx_file' | 'sheet_image' | 'sheet_file' | 'bitable_image' | 'bitable_file'; parent_node?: string; } interface MediaUploadOptions { userAccessToken?: string; } // 工具描述 const description: McpToolDescription = { shortDescription: '飞书-云空间-素材-上传素材,最大20MB, 支持url、文本、base64编码的图片、想当作图片展示的svg, 四选一。注意:除ccm_import_open外,其它上传点需要指定block id,用于指定素材将要上传到的云文档或位置。这意味着你通常需要先创建一个块,然后才能上传素材到这个块中。', bestFor: '上传各种类型的素材到飞书云文档,支持多种格式的图片、文本和文件', notRecommendedFor: '超过20MB的大文件上传,或不支持的文件格式', promptExample: '上传一张base64格式的图片到文档中', usageExample: 'drive_media_upload({base64: "data:image/png;base64,...", file_name: "image.png", parent_type: "docx_image", parent_node: "block_id"})', returns: '上传成功的文件token,用于后续在文档中引用该素材' }; export const mediaUploadTool = { project: 'drive', name: 'drive_media_upload', accessTokens: ['user', 'tenant'], description: convertDescriptionToString(description), schema: { url: z.string().describe('文件URL').optional(), text: z.string().describe('文本内容').optional(), base64: z.string().describe('base64编码的图片').optional(), svg2png: z.string().describe('想当作图片展示的svg,其它情况使用text').optional(), file_name: z.string().max(250).describe('文件名'), parent_type: z.enum(['ccm_import_open', 'docx_image', 'docx_file', 'sheet_image', 'sheet_file', 'bitable_image', 'bitable_file']) .describe('上传点的类型。你可根据上传的素材类型与云文档类型确定上传点类型。例如,要将一张图片插入到新版文档(文件类型为 docx)中,需指定上传点为 docx_image;要将一个附件上传到新版文档中,需指定上传点为 docx_file。ccm_import_open是导入文档专用,别的文件不要用,否则会报错。') .optional(), parent_node: z.string() .describe('block id,即要在文档中插入的块的id,用于指定素材将要上传到的云文档或位置。此处不能为文档ID,当parent_type不是ccm_import_open时,parent_node一定得是块的id,如果你提供不了,就放弃调用这个TOOL,先去创建一个块,再调用这个TOOL。千万不要强行调用这个TOOL,没有任何用处。') .optional() }, customHandler: async (params: MediaUploadParams, options?: MediaUploadOptions): Promise<any> => { try { const { userAccessToken } = options || {}; if (!userAccessToken) { throw new Error('用户访问令牌未提供'); } // 验证参数:必须提供四种内容之一 const contentTypes = [params.url, params.text, params.base64, params.svg2png].filter(Boolean); if (contentTypes.length === 0) { throw new Error('必须提供url、text、base64或svg2png中的至少一种内容'); } if (contentTypes.length > 1) { throw new Error('只能提供url、text、base64、svg2png中的一种内容'); } let file: File = new File([], ''); let actualFileName = params.file_name; // 处理不同类型的内容 if (params.base64) { file = new File([Buffer.from(params.base64, 'base64')], params.file_name); } else if (params.svg2png) { // 将svg转换为png使用在线服务 const response = await fetch('https://svg-to-png.tapeless.eu.org', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ svg: params.svg2png }) }); if (!response.ok) { throw new Error(`SVG转PNG失败: ${response.statusText}`); } const pngBuffer = await response.arrayBuffer(); // 创建png文件,如果文件名不是png结尾,则添加.png后缀 actualFileName = params.file_name.endsWith('.png') ? params.file_name : params.file_name + '.png'; file = new File([pngBuffer], actualFileName); } else if (params.url) { // url有值,先下载文件 const response = await fetch(params.url); if (!response.ok) { throw new Error(`下载文件失败: ${response.statusText}`); } file = new File([await response.blob()], params.file_name); } else if (params.text) { // 处理 markdown 内容,为 mermaid 代码块添加标记 let processedText = params.text; if (params.file_name.endsWith('.md')) { processedText = addMermaidBlockMarkers(params.text); } file = new File([processedText], params.file_name); } // 构造 FormData const formData = new FormData(); formData.append('file_name', actualFileName); formData.append('parent_type', params.parent_type || 'ccm_import_open'); formData.append('parent_node', params.parent_node || ''); formData.append('size', file.size.toString()); formData.append('file', file); // 发起 POST 请求 const resp = await fetch('https://open.feishu.cn/open-apis/drive/v1/medias/upload_all', { method: 'POST', headers: { Authorization: `Bearer ${userAccessToken}`, // Content-Type 不需手动设置,fetch 会自动添加 multipart 边界 }, body: formData, }); const result = await resp.json() as any; if (!resp.ok) { throw new Error(`上传请求失败: ${resp.status} ${resp.statusText}, ${JSON.stringify(result)}`); } const response = result.data; if (!response?.file_token) { throw new Error(`上传失败,未获取到文件token: ${JSON.stringify(result)}`); } return { content: [ { type: 'text' as const, text: JSON.stringify({ file_token: response.file_token, file_name: actualFileName, file_size: file.size }, null, 2), }, ], }; } catch (error) { console.error('上传素材失败:', error); return { isError: true, content: [ { type: 'text' as const, text: `上传素材失败: ${error instanceof Error ? error.message : JSON.stringify(error)}`, }, ], }; } }, }; // 导出评论相关工具 export { driveCommentCreate, driveCommentList, driveCommentPatch, driveCommentReplyCreate, driveCommentReplyList } from './comment';

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Xumingmingming/feishu-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server