import { McpTool } from '../../../../types';
import * as lark from '@larksuiteoapi/node-sdk';
import { ReadStream } from 'fs';
import { Readable } from 'stream';
import { z } from 'zod';
// 工具名称类型
export type docxBuiltinToolName = 'docx.builtin.search' | 'docx.builtin.import';
export const larkDocxBuiltinSearchTool: McpTool = {
project: 'docx',
name: 'docx.builtin.search',
accessTokens: ['user'],
description: '[飞书/Lark] - 云文档-文档 - 搜索文档 - 搜索云文档,只支持user_access_token',
schema: {
data: z.object({
search_key: z.string().describe('搜索关键词'),
count: z.number().describe('指定搜索返回的文件数量。取值范围为 [0,50]。').optional(),
offset: z
.number()
.describe(
'指定搜索的偏移量,该参数最小为 0,即不偏移。该参数的值与返回的文件数量之和不得大于或等于 200(即 offset + count < 200)。',
)
.optional(),
owner_ids: z.array(z.string()).describe('文件所有者的 Open ID').optional(),
chat_ids: z.array(z.string()).describe('文件所在群的 ID').optional(),
docs_types: z
.array(z.enum(['doc', 'sheet', 'slides', 'bitable', 'mindnote', 'file']))
.describe(
'文件类型,支持以下枚举:doc:旧版文档;sheet:电子表格;slides:幻灯片;bitable:多维表格;mindnote:思维笔记;file:文件',
)
.optional(),
}),
useUAT: z.boolean().describe('是否使用用户身份请求,false则使用应用身份请求').optional(),
},
customHandler: async (client, params, options): Promise<any> => {
try {
const { userAccessToken } = options || {};
if (!userAccessToken) {
return {
isError: true,
content: [{ type: 'text' as const, text: JSON.stringify({ msg: '当前未配置 userAccessToken' }) }],
};
}
const response = await client.request(
{
method: 'POST',
url: '/open-apis/suite/docs-api/search/object',
data: params.data,
},
lark.withUserAccessToken(userAccessToken),
);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(response.data ?? response),
},
],
};
} catch (error) {
return {
isError: true,
content: [
{
type: 'text' as const,
text: JSON.stringify((error as any).response.data),
},
],
};
}
},
};
export const larkDocxBuiltinImportTool: McpTool = {
project: 'docx',
name: 'docx.builtin.import',
accessTokens: ['user', 'tenant'],
description: '[飞书/Lark] - 云文档-文档 - 导入文档 - 导入云文档,最大20MB',
schema: {
data: z
.object({
markdown: z.string().describe('markdown文件内容'),
file_name: z.string().describe('文件名').max(27).optional(),
})
.describe('请求体'),
useUAT: z.boolean().describe('使用用户身份请求,否则为应用身份').optional(),
},
customHandler: async (client, params, options): Promise<any> => {
try {
const { userAccessToken } = options || {};
const file = Readable.from(params.data.markdown) as ReadStream;
const data = {
file_name: 'docx.md',
parent_type: 'ccm_import_open' as const,
parent_node: '/',
size: Buffer.byteLength(params.data.markdown),
file,
extra: JSON.stringify({ obj_type: 'docx', file_extension: 'md' }),
};
const response =
userAccessToken && params.useUAT
? await client.drive.media.uploadAll({ data }, lark.withUserAccessToken(userAccessToken))
: await client.drive.media.uploadAll({ data });
if (!response?.file_token) {
return {
isError: true,
content: [{ type: 'text' as const, text: JSON.stringify({ msg: '导入文档失败,请检查markdown文件内容' }) }],
};
}
const importData = {
file_extension: 'md',
file_name: params.data.file_name,
file_token: response?.file_token,
type: 'docx',
point: {
mount_type: 1,
mount_key: '',
},
};
const importResponse =
userAccessToken && params.useUAT
? await client.drive.importTask.create({ data: importData }, lark.withUserAccessToken(userAccessToken))
: await client.drive.importTask.create({ data: importData });
const taskId = importResponse.data?.ticket;
if (!taskId) {
return {
isError: true,
content: [{ type: 'text' as const, text: JSON.stringify({ msg: '导入文档失败,请检查markdown文件内容' }) }],
};
}
for (let i = 0; i < 5; i++) {
const taskResponse =
userAccessToken && params.useUAT
? await client.drive.importTask.get({ path: { ticket: taskId } }, lark.withUserAccessToken(userAccessToken))
: await client.drive.importTask.get({ path: { ticket: taskId } });
if (taskResponse.data?.result?.job_status === 0) {
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(taskResponse.data ?? taskResponse),
},
],
};
} else if (taskResponse.data?.result?.job_status !== 1 && taskResponse.data?.result?.job_status !== 2) {
return {
content: [{ type: 'text' as const, text: JSON.stringify(taskResponse.data) }],
};
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
return {
content: [
{
type: 'text' as const,
text: JSON.stringify({ msg: '导入文档失败,请稍后再试' }),
},
],
};
} catch (error) {
return {
isError: true,
content: [
{
type: 'text' as const,
text: JSON.stringify((error as any)?.response?.data || error),
},
],
};
}
},
};
export const docxBuiltinTools = [larkDocxBuiltinSearchTool, larkDocxBuiltinImportTool];