tools.ts 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
  1. // 进程内 MCP 工具(Claude 专用);M2 起同样的能力经 MCP Gateway 提供给 Codex
  2. import path from 'node:path'
  3. import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk'
  4. import { z } from 'zod'
  5. import { CONVERT_FORMATS, convertDocument, recalcWorkbook } from '../../services/office'
  6. export const OFFICE_SERVER = 'office'
  7. export const CONVERT_TOOL = `mcp__${OFFICE_SERVER}__convert_document`
  8. export const RECALC_TOOL = `mcp__${OFFICE_SERVER}__recalc_workbook`
  9. export function officeServer(workspaceDir: string, pythonRuntime: string) {
  10. return createSdkMcpServer({
  11. name: OFFICE_SERVER,
  12. version: '0.1.0',
  13. tools: [
  14. tool(
  15. 'convert_document',
  16. '用 LibreOffice 转换文档格式(例如 docx→pdf、xlsx→pdf、pptx→pdf、doc→docx、html→pdf)。'
  17. + '输入和输出都必须在工作区内;输出文件名与输入相同、扩展名换成目标格式。不要在 Bash 里直接调用 soffice,它在沙箱中无法运行。',
  18. {
  19. input: z.string().describe('输入文件路径(相对工作区或绝对路径)'),
  20. format: z.enum(CONVERT_FORMATS).describe('目标格式'),
  21. out_dir: z.string().optional().describe('输出目录,默认 outputs'),
  22. },
  23. async args => {
  24. try {
  25. const out = await convertDocument({ workspaceDir, input: args.input, format: args.format, outDir: args.out_dir })
  26. return { content: [{ type: 'text', text: `已生成:${path.relative(workspaceDir, out)}` }] }
  27. } catch (err) {
  28. return { content: [{ type: 'text', text: (err as Error).message }], isError: true }
  29. }
  30. },
  31. ),
  32. tool(
  33. 'recalc_workbook',
  34. '用 LibreOffice 重新计算 Excel 文件中的所有公式并写回原文件,然后报告公式数量和错误单元格(#REF!、#DIV/0! 等)。'
  35. + '用 openpyxl 写入公式后必须调用它,否则文件里没有计算结果。不要在 Bash 里调用 soffice 或 recalc.py,它们在沙箱中无法运行。',
  36. { input: z.string().describe('xlsx/xlsm 文件路径(相对工作区或绝对路径)') },
  37. async args => {
  38. try {
  39. const r = await recalcWorkbook({ workspaceDir, input: args.input, pythonRuntime })
  40. const lines = [`已重算:${path.relative(workspaceDir, r.path)},公式 ${r.formulas} 个,错误 ${r.errorCount} 个`]
  41. for (const e of r.errors) lines.push(`- ${e.sheet}!${e.cell}: ${e.error}`)
  42. return { content: [{ type: 'text', text: lines.join('\n') }], isError: r.errorCount > 0 }
  43. } catch (err) {
  44. return { content: [{ type: 'text', text: (err as Error).message }], isError: true }
  45. }
  46. },
  47. ),
  48. ],
  49. })
  50. }