server.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. #!/usr/bin/env node
  2. import { Server } from "@modelcontextprotocol/sdk/server/index.js";
  3. import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  4. import {
  5. CallToolRequestSchema,
  6. ListToolsRequestSchema,
  7. McpError,
  8. ErrorCode,
  9. } from "@modelcontextprotocol/sdk/types.js";
  10. import path from "node:path";
  11. import { fileURLToPath } from "node:url";
  12. import {
  13. FeishuClient,
  14. inferExportType,
  15. resolveWorkspacePath,
  16. } from "./feishu-client.js";
  17. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  18. const WORKSPACE_ROOT =
  19. process.env.FEISHU_WORKSPACE_ROOT ||
  20. path.resolve(__dirname, "..", "..");
  21. const DEFAULT_FOLDER = process.env.FEISHU_DEFAULT_FOLDER_TOKEN || "";
  22. let client;
  23. try {
  24. client = FeishuClient.fromEnv();
  25. } catch (error) {
  26. console.error("[feishu-drive-mcp] 初始化失败:", error.message);
  27. process.exit(1);
  28. }
  29. function textResult(obj) {
  30. return {
  31. content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
  32. };
  33. }
  34. function toolError(message) {
  35. return {
  36. isError: true,
  37. content: [{ type: "text", text: message }],
  38. };
  39. }
  40. const server = new Server(
  41. { name: "feishu-drive-mcp", version: "0.1.0" },
  42. { capabilities: { tools: {} } }
  43. );
  44. server.setRequestHandler(ListToolsRequestSchema, async () => ({
  45. tools: [
  46. {
  47. name: "feishu_list_folder",
  48. description:
  49. "列出飞书云盘文件夹内容。返回 file_token、名称、类型(docx/sheet/file/folder 等)。folder_token 不传则用 FEISHU_DEFAULT_FOLDER_TOKEN。",
  50. inputSchema: {
  51. type: "object",
  52. properties: {
  53. folder_token: {
  54. type: "string",
  55. description: "文件夹 token,如链接 Ks1RfygMblbsNEdB3JdcxHCnnFb",
  56. },
  57. page_token: { type: "string", description: "分页 token" },
  58. },
  59. required: [],
  60. },
  61. },
  62. {
  63. name: "feishu_get_file_meta",
  64. description: "获取云盘文件或在线文档的元信息(名称、类型、父文件夹等)",
  65. inputSchema: {
  66. type: "object",
  67. properties: {
  68. file_token: { type: "string", description: "文件 token" },
  69. doc_type: {
  70. type: "string",
  71. description: "可选:file/docx/doc/sheet/bitable/slides/folder",
  72. },
  73. },
  74. required: ["file_token"],
  75. },
  76. },
  77. {
  78. name: "feishu_download_file",
  79. description:
  80. "下载云盘中的 Office 文件(docx/xlsx/pptx 等 file 类型)到工作区相对路径或绝对路径",
  81. inputSchema: {
  82. type: "object",
  83. properties: {
  84. file_token: { type: "string" },
  85. save_path: {
  86. type: "string",
  87. description: "保存路径,相对仓库根或绝对路径",
  88. },
  89. },
  90. required: ["file_token", "save_path"],
  91. },
  92. },
  93. {
  94. name: "feishu_upload_file",
  95. description:
  96. "上传本地 Office 文件到飞书云盘文件夹;可选 replace_file_token 覆盖旧文件",
  97. inputSchema: {
  98. type: "object",
  99. properties: {
  100. local_path: { type: "string", description: "本地文件路径" },
  101. folder_token: {
  102. type: "string",
  103. description: "目标文件夹 token,覆盖时可省略",
  104. },
  105. file_name: { type: "string", description: "上传后的文件名" },
  106. replace_file_token: {
  107. type: "string",
  108. description: "若提供,上传到同目录后删除此旧文件",
  109. },
  110. },
  111. required: ["local_path"],
  112. },
  113. },
  114. {
  115. name: "feishu_export_online_doc",
  116. description:
  117. "将飞书在线文档/表格/幻灯片导出为 Office 文件并下载(docx/xlsx/pptx/pdf 等)",
  118. inputSchema: {
  119. type: "object",
  120. properties: {
  121. file_token: { type: "string", description: "在线文档 token" },
  122. file_extension: {
  123. type: "string",
  124. description: "导出格式:docx/pdf/xlsx/csv/pptx 等",
  125. },
  126. save_path: { type: "string", description: "保存路径" },
  127. },
  128. required: ["file_token", "file_extension", "save_path"],
  129. },
  130. },
  131. {
  132. name: "feishu_delete_file",
  133. description: "删除云盘文件(慎用)",
  134. inputSchema: {
  135. type: "object",
  136. properties: {
  137. file_token: { type: "string" },
  138. },
  139. required: ["file_token"],
  140. },
  141. },
  142. ],
  143. }));
  144. server.setRequestHandler(CallToolRequestSchema, async (request) => {
  145. const { name, arguments: args } = request.params;
  146. try {
  147. switch (name) {
  148. case "feishu_list_folder": {
  149. const folderToken = args?.folder_token || DEFAULT_FOLDER;
  150. if (!folderToken) {
  151. throw new McpError(
  152. ErrorCode.InvalidParams,
  153. "请提供 folder_token 或配置 FEISHU_DEFAULT_FOLDER_TOKEN"
  154. );
  155. }
  156. const data = await client.listFolder(folderToken, args?.page_token);
  157. return textResult(data);
  158. }
  159. case "feishu_get_file_meta": {
  160. const data = await client.getFileMeta(
  161. args.file_token,
  162. args.doc_type
  163. );
  164. return textResult(data);
  165. }
  166. case "feishu_download_file": {
  167. const savePath = resolveWorkspacePath(args.save_path, WORKSPACE_ROOT);
  168. const data = await client.downloadFile(args.file_token, savePath);
  169. return textResult(data);
  170. }
  171. case "feishu_upload_file": {
  172. const localPath = resolveWorkspacePath(args.local_path, WORKSPACE_ROOT);
  173. const folderToken = args.folder_token || DEFAULT_FOLDER || undefined;
  174. if (!folderToken && !args.replace_file_token) {
  175. throw new McpError(
  176. ErrorCode.InvalidParams,
  177. "请提供 folder_token、replace_file_token 或配置 FEISHU_DEFAULT_FOLDER_TOKEN"
  178. );
  179. }
  180. const data = await client.uploadFile({
  181. folderToken,
  182. localPath,
  183. fileName: args.file_name,
  184. replaceFileToken: args.replace_file_token,
  185. });
  186. return textResult(data);
  187. }
  188. case "feishu_export_online_doc": {
  189. const meta = await client.getFileMeta(args.file_token);
  190. const hint = inferExportType(meta.type);
  191. if (!hint) {
  192. throw new McpError(
  193. ErrorCode.InvalidParams,
  194. `类型 ${meta.type} 不支持导出,请用 feishu_download_file 或官方 lark-mcp 在线编辑`
  195. );
  196. }
  197. if (!hint.extensions.includes(args.file_extension)) {
  198. throw new McpError(
  199. ErrorCode.InvalidParams,
  200. `${meta.type} 支持的导出格式: ${hint.extensions.join(", ")}`
  201. );
  202. }
  203. const savePath = resolveWorkspacePath(args.save_path, WORKSPACE_ROOT);
  204. const data = await client.exportAndDownload({
  205. token: args.file_token,
  206. type: hint.type,
  207. fileExtension: args.file_extension,
  208. savePath,
  209. });
  210. return textResult({ ...data, source_type: meta.type, name: meta.name });
  211. }
  212. case "feishu_delete_file": {
  213. await client.deleteFile(args.file_token);
  214. return textResult({ deleted: args.file_token });
  215. }
  216. default:
  217. throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
  218. }
  219. } catch (error) {
  220. if (error instanceof McpError) {
  221. throw error;
  222. }
  223. return toolError(error.message || String(error));
  224. }
  225. });
  226. async function main() {
  227. const transport = new StdioServerTransport();
  228. await server.connect(transport);
  229. }
  230. main().catch((error) => {
  231. console.error("[feishu-drive-mcp] fatal:", error);
  232. process.exit(1);
  233. });