/**
* Feishu Media Tools
*
* MCP tools for uploading and inserting images and callout blocks into Feishu documents.
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { z } from 'zod'
import { readFileSync } from 'fs'
import { basename } from 'path'
import { getApiClient } from '../services/api.js'
import { extractDocumentId } from '../constants.js'
export function registerMediaTools(server: McpServer): void {
const api = getApiClient()
// ============================================================================
// feishu_upload_image - Upload an image to Feishu
// ============================================================================
server.tool(
'feishu_upload_image',
'上传图片到飞书。返回 file_token,可用于 feishu_insert_image 插入文档。⚠️ 需要 drive:drive:media:upload 权限。',
{
file_path: z.string().min(1).describe('本地图片文件路径'),
document_id: z.string().optional().describe('目标文档 ID(可选,用于关联文档)'),
},
async (args) => {
try {
const { file_path, document_id } = args
// Read file
const fileBuffer = readFileSync(file_path)
const fileName = basename(file_path)
// Upload to Feishu
const docId = document_id ? extractDocumentId(document_id) : ''
const fileToken = await api.uploadMedia(fileBuffer, fileName, 'docx_image', docId)
return {
content: [
{
type: 'text' as const,
text: `✅ 图片上传成功!\n\n📷 文件:${fileName}\n🔑 Token:${fileToken}\n\n使用 feishu_insert_image 工具并传入此 token 即可插入文档。`,
},
],
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return {
content: [
{
type: 'text' as const,
text: `上传图片失败: ${message}`,
},
],
isError: true,
}
}
}
)
// ============================================================================
// feishu_insert_callout - Insert a quote container block
// ============================================================================
server.tool(
'feishu_insert_callout',
'在飞书文档中插入高亮块(Callout)。用于突出显示提示、警告、注意事项等信息。',
{
document_id: z.string().min(1).describe('飞书文档 ID 或 URL'),
content: z.string().min(1).describe('高亮块内容'),
background_color: z
.enum(['grey', 'gray', 'red', 'orange', 'yellow', 'green', 'blue', 'purple'])
.optional()
.describe('背景颜色,默认灰色'),
icon: z
.enum(['info', 'warning', 'error', 'success', 'tip', 'note', 'important', 'question'])
.optional()
.describe('图标类型'),
custom_emoji: z.string().optional().describe('自定义 emoji 图标(覆盖 icon)'),
block_id: z.string().optional().describe('插入位置的 Block ID,不指定则追加到文档末尾'),
},
async (args) => {
try {
const { document_id, content, background_color, icon, custom_emoji, block_id } = args
const docId = extractDocumentId(document_id)
// Get all blocks to find insertion point
const blocks = await api.getAllBlocks(docId)
const pageBlock = blocks.find((b) => b.block_type === 1)
if (!pageBlock) {
throw new Error('无法找到文档根节点')
}
// Determine insertion position
let parentBlockId = pageBlock.block_id
let insertIndex: number | undefined
if (block_id) {
const targetBlock = blocks.find((b) => b.block_id === block_id)
if (targetBlock) {
parentBlockId = targetBlock.parent_id
const siblings = blocks.filter((b) => b.parent_id === parentBlockId)
const targetIndex = siblings.findIndex((b) => b.block_id === block_id)
if (targetIndex !== -1) {
insertIndex = targetIndex + 1
}
}
}
// Build callout block (without children - they need to be added separately)
// Note: Callout uses 'quote_container' as property name in Feishu API
const calloutBlock = {
block_type: 34, // Callout block type
quote_container: {},
}
const createResult = await api.createBlocks(docId, parentBlockId, [calloutBlock], insertIndex)
// Add text content inside the callout block
if (createResult && createResult.length > 0) {
const calloutBlockId = createResult[0].block_id
if (calloutBlockId) {
await api.createBlocks(docId, calloutBlockId, [{
block_type: 2, // Text block
text: {
elements: [{ text_run: { content } }],
},
}])
}
}
return {
content: [
{
type: 'text' as const,
text: `✅ 高亮块插入成功!\n\n🎨 背景色:${background_color || 'grey'}\n📝 内容:${content.slice(0, 50)}${content.length > 50 ? '...' : ''}`,
},
],
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return {
content: [
{
type: 'text' as const,
text: `插入高亮块失败: ${message}`,
},
],
isError: true,
}
}
}
)
// ============================================================================
// feishu_insert_image - Insert an image block
// ============================================================================
server.tool(
'feishu_insert_image',
'在飞书文档中插入图片。支持通过文件 token 插入已上传的图片。',
{
document_id: z.string().min(1).describe('飞书文档 ID 或 URL'),
file_token: z.string().min(1).describe('图片文件 token(需先通过飞书上传接口获取)'),
width: z.number().optional().describe('图片宽度(像素)'),
height: z.number().optional().describe('图片高度(像素)'),
block_id: z.string().optional().describe('插入位置的 Block ID,不指定则追加到文档末尾'),
},
async (args) => {
try {
const { document_id, file_token, width, height, block_id } = args
const docId = extractDocumentId(document_id)
// Get all blocks to find insertion point
const blocks = await api.getAllBlocks(docId)
const pageBlock = blocks.find((b) => b.block_type === 1)
if (!pageBlock) {
throw new Error('无法找到文档根节点')
}
// Determine insertion position
let parentBlockId = pageBlock.block_id
let insertIndex: number | undefined
if (block_id) {
const targetBlock = blocks.find((b) => b.block_id === block_id)
if (targetBlock) {
parentBlockId = targetBlock.parent_id
const siblings = blocks.filter((b) => b.parent_id === parentBlockId)
const targetIndex = siblings.findIndex((b) => b.block_id === block_id)
if (targetIndex !== -1) {
insertIndex = targetIndex + 1
}
}
}
// Build image block
const imageBlock: Record<string, unknown> = {
block_type: 27, // Image block type
image: {
token: file_token,
},
}
// Add dimensions if specified
if (width || height) {
;(imageBlock.image as Record<string, unknown>).width = width || 400
;(imageBlock.image as Record<string, unknown>).height = height || 300
}
await api.createBlocks(docId, parentBlockId, [imageBlock], insertIndex)
return {
content: [
{
type: 'text' as const,
text: `✅ 图片插入成功!\n\n📷 Token: ${file_token}\n📐 尺寸: ${width || 'auto'} x ${height || 'auto'}`,
},
],
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return {
content: [
{
type: 'text' as const,
text: `插入图片失败: ${message}`,
},
],
isError: true,
}
}
}
)
}