import { createSdkMcpServer, tool, type McpSdkServerConfigWithInstance } from "@anthropic-ai/claude-agent-sdk"; import { z } from "zod"; import { sendFile, type FeishuRuntime } from "./client.js"; import { resolveDeliverableFile } from "./fileDelivery.js"; import { readFeishuContext } from "./read.js"; export interface FileDeliveryToolOptions { readonly rt: FeishuRuntime; readonly chatId: string; readonly projectId: string; readonly runId: string; readonly workspaceDir: string; readonly onDelivered?: (path: string) => void; } export function createFileDeliveryMcpServer(options: FileDeliveryToolOptions): McpSdkServerConfigWithInstance { return createSdkMcpServer({ name: "cph_hub", version: "0.0.0", alwaysLoad: true, instructions: "Use send_file when the user asks to receive, resend, download, or attach a file. " + "Do not claim a file was sent unless send_file returns success. " + "If a generated file path is uncertain, list or inspect the workspace first, then call send_file with the actual path. " + "Use feishu_read_context when the trigger context contains a reply, thread, or message anchor and more Feishu context is needed.", tools: [ tool( "send_file", "Upload an existing project/repository file to the current Feishu chat. The path must point to a concrete file, for example build/student.pdf or README.md.", { path: z.string().describe("Workspace-relative, repository-relative, or absolute path to the existing file."), name: z.string().optional().describe("Optional display filename. Defaults to the file's basename."), }, async (args) => { const file = await resolveDeliverableFile(args.path, options.workspaceDir); if (file === null) { return { isError: true, content: [{ type: "text", text: `File not found or not deliverable: ${args.path}` }], }; } const messageId = await sendFile(options.rt, options.chatId, file.path, args.name ?? file.name); if (messageId === null) { return { isError: true, content: [{ type: "text", text: `Failed to send file: ${file.path}` }], }; } options.onDelivered?.(file.path); return { content: [{ type: "text", text: `Sent file: ${file.path}` }], }; }, { alwaysLoad: true }, ), tool( "feishu_read_context", "Read Feishu context for the current project's bound chat. Use anchor ids from the trigger context; this tool never reads arbitrary chats.", { anchor: z.enum(["trigger_message", "status_card", "reply", "thread"]).describe("Which kind of Feishu anchor to read."), id: z.string().describe("The message/thread/status anchor id to read."), }, async (args) => { const result = await readFeishuContext( { chat_id: options.chatId, anchor: args.anchor, id: args.id }, { runId: options.runId, projectId: options.projectId, boundChatId: options.chatId, workspaceDir: options.workspaceDir, }, options.rt, ); return { content: [{ type: "text", text: result }] }; }, { alwaysLoad: true }, ), ], }); }