/**
* Bitable (多维表格) tools for Feishu MCP Server
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { z } from 'zod'
import { getApiClient } from '../services/api.js'
// Schemas
const CreateBitableSchema = z.object({
name: z.string().min(1).max(255).describe('多维表格名称'),
folder_token: z.string().optional().describe('目标文件夹 Token'),
})
const AddBitableRecordsSchema = z.object({
app_token: z.string().min(1).describe('多维表格 app_token 或完整 URL'),
table_id: z.string().optional().describe('数据表 ID,不指定则使用默认表'),
records: z.array(z.record(z.string(), z.any())).describe('记录数组,如 [{"名称":"张三","状态":"完成"}]'),
})
const ReadBitableSchema = z.object({
app_token: z.string().min(1).describe('多维表格 app_token 或完整 URL'),
table_id: z.string().optional().describe('数据表 ID,不指定则使用默认表'),
page_size: z.number().optional().describe('每页记录数,默认 20'),
})
type CreateBitableArgs = z.infer<typeof CreateBitableSchema>
type AddBitableRecordsArgs = z.infer<typeof AddBitableRecordsSchema>
type ReadBitableArgs = z.infer<typeof ReadBitableSchema>
function extractAppToken(input: string): string {
const urlMatch = input.match(/\/base\/([a-zA-Z0-9]+)/)
if (urlMatch) return urlMatch[1]
return input
}
export function registerBitableTools(server: McpServer): void {
const api = getApiClient()
// feishu_create_bitable
server.tool(
'feishu_create_bitable',
'创建新的飞书多维表格(Bitable)。返回 app_token 和 URL。',
CreateBitableSchema.shape,
async (args: CreateBitableArgs) => {
try {
const { name, folder_token } = args
const body: Record<string, string> = { name }
if (folder_token) body.folder_token = folder_token
const result = await api.request<{
app: { app_token: string; name: string; url: string; default_table_id: string }
}>('POST', '/bitable/v1/apps', body)
const app = result.app
return {
content: [{
type: 'text' as const,
text: `多维表格创建成功!\n\n📊 名称: ${app.name}\n🔗 链接: ${app.url}\n📋 app_token: ${app.app_token}\n📋 default_table_id: ${app.default_table_id}`,
}],
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return { content: [{ type: 'text' as const, text: `创建多维表格失败: ${message}` }], isError: true }
}
}
)
// feishu_add_bitable_records
server.tool(
'feishu_add_bitable_records',
'向飞书多维表格添加记录。字段名需与表格字段名一致。',
AddBitableRecordsSchema.shape,
async (args: AddBitableRecordsArgs) => {
try {
const { app_token, table_id, records } = args
const token = extractAppToken(app_token)
// Get table_id if not provided
let targetTableId = table_id
if (!targetTableId) {
const tablesResult = await api.request<{
items: Array<{ table_id: string; name: string }>
}>('GET', `/bitable/v1/apps/${token}/tables`)
targetTableId = tablesResult.items[0].table_id
}
// Add records one by one (batch API has different format)
const results: string[] = []
for (const record of records) {
const result = await api.request<{
record: { record_id: string }
}>('POST', `/bitable/v1/apps/${token}/tables/${targetTableId}/records`, {
fields: record
})
results.push(result.record.record_id)
}
return {
content: [{
type: 'text' as const,
text: `成功添加 ${results.length} 条记录!\n\n记录 ID: ${results.join(', ')}`,
}],
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return { content: [{ type: 'text' as const, text: `添加记录失败: ${message}` }], isError: true }
}
}
)
// feishu_read_bitable
server.tool(
'feishu_read_bitable',
'读取飞书多维表格的记录。返回 Markdown 格式的表格。',
ReadBitableSchema.shape,
async (args: ReadBitableArgs) => {
try {
const { app_token, table_id, page_size } = args
const token = extractAppToken(app_token)
let targetTableId = table_id
if (!targetTableId) {
const tablesResult = await api.request<{
items: Array<{ table_id: string; name: string }>
}>('GET', `/bitable/v1/apps/${token}/tables`)
targetTableId = tablesResult.items[0].table_id
}
// Get fields first
const fieldsResult = await api.request<{
items: Array<{ field_id: string; field_name: string; type: number }>
}>('GET', `/bitable/v1/apps/${token}/tables/${targetTableId}/fields`)
const fieldNames = fieldsResult.items.map(f => f.field_name)
// Get records
const recordsResult = await api.request<{
items: Array<{ record_id: string; fields: Record<string, unknown> }>
total: number
}>('GET', `/bitable/v1/apps/${token}/tables/${targetTableId}/records`, undefined, {
page_size: String(page_size || 20)
})
const records = recordsResult.items || []
// Format as markdown table
let markdown = `共 ${recordsResult.total} 条记录\n\n`
if (records.length > 0) {
markdown += '| ' + fieldNames.join(' | ') + ' |\n'
markdown += '| ' + fieldNames.map(() => '---').join(' | ') + ' |\n'
for (const record of records) {
const row = fieldNames.map(name => {
const val = record.fields[name]
if (val === null || val === undefined) return ''
if (typeof val === 'object') return JSON.stringify(val)
return String(val)
})
markdown += '| ' + row.join(' | ') + ' |\n'
}
}
return { content: [{ type: 'text' as const, text: markdown }] }
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
return { content: [{ type: 'text' as const, text: `读取记录失败: ${message}` }], isError: true }
}
}
)
}