#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode, } from "@modelcontextprotocol/sdk/types.js"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { FeishuClient, inferExportType, resolveWorkspacePath, } from "./feishu-client.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const WORKSPACE_ROOT = process.env.FEISHU_WORKSPACE_ROOT || path.resolve(__dirname, "..", ".."); const DEFAULT_FOLDER = process.env.FEISHU_DEFAULT_FOLDER_TOKEN || ""; let client; try { client = FeishuClient.fromEnv(); } catch (error) { console.error("[feishu-drive-mcp] 初始化失败:", error.message); process.exit(1); } function textResult(obj) { return { content: [{ type: "text", text: JSON.stringify(obj, null, 2) }], }; } function toolError(message) { return { isError: true, content: [{ type: "text", text: message }], }; } const server = new Server( { name: "feishu-drive-mcp", version: "0.1.0" }, { capabilities: { tools: {} } } ); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "feishu_list_folder", description: "列出飞书云盘文件夹内容。返回 file_token、名称、类型(docx/sheet/file/folder 等)。folder_token 不传则用 FEISHU_DEFAULT_FOLDER_TOKEN。", inputSchema: { type: "object", properties: { folder_token: { type: "string", description: "文件夹 token,如链接 Ks1RfygMblbsNEdB3JdcxHCnnFb", }, page_token: { type: "string", description: "分页 token" }, }, required: [], }, }, { name: "feishu_get_file_meta", description: "获取云盘文件或在线文档的元信息(名称、类型、父文件夹等)", inputSchema: { type: "object", properties: { file_token: { type: "string", description: "文件 token" }, doc_type: { type: "string", description: "可选:file/docx/doc/sheet/bitable/slides/folder", }, }, required: ["file_token"], }, }, { name: "feishu_download_file", description: "下载云盘中的 Office 文件(docx/xlsx/pptx 等 file 类型)到工作区相对路径或绝对路径", inputSchema: { type: "object", properties: { file_token: { type: "string" }, save_path: { type: "string", description: "保存路径,相对仓库根或绝对路径", }, }, required: ["file_token", "save_path"], }, }, { name: "feishu_upload_file", description: "上传本地 Office 文件到飞书云盘文件夹;可选 replace_file_token 覆盖旧文件", inputSchema: { type: "object", properties: { local_path: { type: "string", description: "本地文件路径" }, folder_token: { type: "string", description: "目标文件夹 token,覆盖时可省略", }, file_name: { type: "string", description: "上传后的文件名" }, replace_file_token: { type: "string", description: "若提供,上传到同目录后删除此旧文件", }, }, required: ["local_path"], }, }, { name: "feishu_export_online_doc", description: "将飞书在线文档/表格/幻灯片导出为 Office 文件并下载(docx/xlsx/pptx/pdf 等)", inputSchema: { type: "object", properties: { file_token: { type: "string", description: "在线文档 token" }, file_extension: { type: "string", description: "导出格式:docx/pdf/xlsx/csv/pptx 等", }, save_path: { type: "string", description: "保存路径" }, }, required: ["file_token", "file_extension", "save_path"], }, }, { name: "feishu_delete_file", description: "删除云盘文件(慎用)", inputSchema: { type: "object", properties: { file_token: { type: "string" }, }, required: ["file_token"], }, }, ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "feishu_list_folder": { const folderToken = args?.folder_token || DEFAULT_FOLDER; if (!folderToken) { throw new McpError( ErrorCode.InvalidParams, "请提供 folder_token 或配置 FEISHU_DEFAULT_FOLDER_TOKEN" ); } const data = await client.listFolder(folderToken, args?.page_token); return textResult(data); } case "feishu_get_file_meta": { const data = await client.getFileMeta( args.file_token, args.doc_type ); return textResult(data); } case "feishu_download_file": { const savePath = resolveWorkspacePath(args.save_path, WORKSPACE_ROOT); const data = await client.downloadFile(args.file_token, savePath); return textResult(data); } case "feishu_upload_file": { const localPath = resolveWorkspacePath(args.local_path, WORKSPACE_ROOT); const folderToken = args.folder_token || DEFAULT_FOLDER || undefined; if (!folderToken && !args.replace_file_token) { throw new McpError( ErrorCode.InvalidParams, "请提供 folder_token、replace_file_token 或配置 FEISHU_DEFAULT_FOLDER_TOKEN" ); } const data = await client.uploadFile({ folderToken, localPath, fileName: args.file_name, replaceFileToken: args.replace_file_token, }); return textResult(data); } case "feishu_export_online_doc": { const meta = await client.getFileMeta(args.file_token); const hint = inferExportType(meta.type); if (!hint) { throw new McpError( ErrorCode.InvalidParams, `类型 ${meta.type} 不支持导出,请用 feishu_download_file 或官方 lark-mcp 在线编辑` ); } if (!hint.extensions.includes(args.file_extension)) { throw new McpError( ErrorCode.InvalidParams, `${meta.type} 支持的导出格式: ${hint.extensions.join(", ")}` ); } const savePath = resolveWorkspacePath(args.save_path, WORKSPACE_ROOT); const data = await client.exportAndDownload({ token: args.file_token, type: hint.type, fileExtension: args.file_extension, savePath, }); return textResult({ ...data, source_type: meta.type, name: meta.name }); } case "feishu_delete_file": { await client.deleteFile(args.file_token); return textResult({ deleted: args.file_token }); } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { if (error instanceof McpError) { throw error; } return toolError(error.message || String(error)); } }); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); } main().catch((error) => { console.error("[feishu-drive-mcp] fatal:", error); process.exit(1); });