import { type McpServer } from "@modelcontextprotocol/server"; import { z } from "zod/v4"; import { backfillResourceProfile, collectNow, recoveryExport, recoveryList, resourceExport, resourceGet, resourceSearch, retryFailed, runDueCollections, setCollectionPlan, taskGet, taskList, type McpOperationBindings, } from "./mcp-operations"; type Options = { bindings: McpOperationBindings; origin: string; }; function result(label: string, data: unknown) { return { content: [{ type: "text" as const, text: label }], structuredContent: data as Record, }; } function errorResult(error: unknown) { const message = error instanceof Error ? error.message : "操作失败,请稍后重试"; return { isError: true as const, content: [{ type: "text" as const, text: message.slice(0, 240) }], }; } function withError(handler: (...args: T) => Promise>) { return async (...args: T) => { try { return await handler(...args); } catch (error) { return errorResult(error); } }; } const pagination = { limit: z.number().int().min(1).max(200).optional().describe("返回数量,默认50,最大200"), offset: z.number().int().min(0).optional().describe("分页偏移量,默认0"), }; const resourceFilters = { query: z.string().max(100).optional().describe("账号名称、账号号、简介或标签,支持模糊搜索"), ip_location: z.string().max(100).optional().describe("IP地区关键词,支持模糊搜索"), cooperation_source: z.string().max(100).optional().describe("历史合作来源关键词,支持模糊搜索"), platform: z.string().max(30).optional().describe("平台,例如小红书;不传表示全部"), }; export function registerMcpOperationTools(server: McpServer, options: Options) { server.registerTool( "task_list", { title: "查询任务及进度", description: "查询 KOC 分发任务列表、领取数、发布数、第7天回收数及领取链接。", inputSchema: z.object({ query: z.string().max(100).optional().describe("任务名或品牌/项目关键词"), status: z.string().max(30).optional().describe("任务状态;不传或 all 表示全部"), ...pagination, }), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, }, withError(async (input) => { const data = await taskList(input, options.bindings.KOC_PORTAL_URL?.trim() || ""); return result(`共找到 ${data.total} 个任务。`, data); }), ); server.registerTool( "task_get", { title: "查看任务完整情况", description: "查看指定任务、笔记、领取记录、发布回填和采集执行记录。", inputSchema: z.object({ task_id: z.string().min(1).describe("任务ID") }), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, }, withError(async ({ task_id }) => { const data = await taskGet(task_id, options.bindings.KOC_PORTAL_URL?.trim() || ""); return result(`已读取任务“${String(data.task.name)}”的完整情况。`, data); }), ); server.registerTool( "recovery_list", { title: "查询数据回收队列", description: "查询已发布、未回填、待第7天数据或已到第7天仍未回填的笔记。", inputSchema: z.object({ task_id: z.string().optional().describe("任务ID;不传则跨任务查询"), stage: z.enum(["all", "published", "unfilled", "waiting_day7", "day7_due"]).optional(), ...pagination, }), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, }, withError(async ({ task_id, stage, limit, offset }) => { const data = await recoveryList({ taskId: task_id, stage, limit, offset }); return result(`数据回收队列共 ${data.total} 条记录。`, data); }), ); server.registerTool( "recovery_export", { title: "导出任务完整数据", description: "生成任务完整 Excel,包含笔记原图、发布截图、创作者截图和回收数据。", inputSchema: z.object({ task_id: z.string().min(1).describe("任务ID") }), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false }, }, withError(async ({ task_id }) => { const data = await recoveryExport(task_id, options.origin); return result(`导出文件已生成,下载链接将在 ${data.expires_at} 失效。`, data); }), ); server.registerTool( "collection_plan_set", { title: "设置自动采集计划", description: "为任务设置开始日期及第1至第7天的自动采集日,系统在北京时间09:00执行。", inputSchema: z.object({ task_id: z.string().min(1), start_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("北京时间开始日期 YYYY-MM-DD"), days: z.array(z.number().int().min(1).max(7)).min(1).max(7).describe("需要采集的相对天数,例如 [2,5,7]"), }), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, }, withError(async ({ task_id, start_date, days }) => { const data = await setCollectionPlan(task_id, start_date, days, options.bindings); return result(`已为任务设置 ${data.days.length} 个自动采集日。`, data); }), ); server.registerTool( "collection_run_due", { title: "执行到期采集", description: "立即执行今天或此前已到期但尚未成功的自动采集任务。", inputSchema: z.object({ task_id: z.string().optional().describe("任务ID;不传则执行所有到期任务") }), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, }, withError(async ({ task_id }) => { const data = await runDueCollections(task_id, options.bindings); return result(`到期采集完成:成功 ${data.succeeded},失败 ${data.failed}。`, data); }), ); server.registerTool( "collection_collect_now", { title: "立即采集指定笔记", description: "对指定分发记录立即采集互动数据;小红书为点赞/收藏/评论,抖音另含转发。", inputSchema: z.object({ distribution_id: z.string().min(1).describe("分发记录ID,可从 task_get 或 recovery_list 获取"), schedule_day: z.number().int().min(1).max(7).optional().describe("标记为第几天采集,可不传"), }), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, }, withError(async ({ distribution_id, schedule_day }) => { const data = await collectNow(distribution_id, schedule_day, options.bindings); return result("指定笔记采集完成。", data); }), ); server.registerTool( "collection_retry_failed", { title: "补采异常数据", description: "重试指定任务或全部任务中采集状态异常的已发布笔记。", inputSchema: z.object({ task_id: z.string().optional().describe("任务ID;不传则补采最近异常任务") }), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, }, withError(async ({ task_id }) => { const data = await retryFailed(task_id, options.bindings); return result(`补采完成:成功 ${data.succeeded},失败 ${data.failed}。`, data); }), ); server.registerTool( "resource_search", { title: "搜索 KOC 账号资源", description: "按账号名称/账号号/标签、IP地区、合作来源或平台搜索 KOC 资源。", inputSchema: z.object({ ...resourceFilters, ...pagination }), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, }, withError(async ({ query, ip_location, cooperation_source, platform, limit, offset }) => { const data = await resourceSearch({ query, ipLocation: ip_location, cooperationSource: cooperation_source, platform, limit, offset }); return result(`共找到 ${data.total} 个账号。`, data); }), ); server.registerTool( "resource_get", { title: "查看 KOC 账号详情", description: "查看账号主页、账号号、粉丝数、性别、简介、标签、IP地区以及全部合作记录。", inputSchema: z.object({ account_id: z.string().min(1).describe("KOC LOOP 账号ID") }), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }, }, withError(async ({ account_id }) => { const data = await resourceGet(account_id); return result(`已读取账号“${String(data.account.nickname)}”的详情。`, data); }), ); server.registerTool( "resource_backfill_profile", { title: "补全公开账号信息", description: "根据已回填的小红书或抖音作品链接补全账号主页、昵称、账号号、IP地区和粉丝数。", inputSchema: z.object({ distribution_id: z.string().optional().describe("分发记录ID,和发布链接二选一"), publish_url: z.string().optional().describe("小红书或抖音作品链接,也可传包含链接的分享文案"), }).refine((value) => Boolean(value.distribution_id || value.publish_url), "请提供分发记录ID或发布链接"), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }, }, withError(async ({ distribution_id, publish_url }) => { const data = await backfillResourceProfile({ distributionId: distribution_id, publishUrl: publish_url }, options.bindings); return result(data.updated ? "账号公开信息已补全。" : "账号信息未发生变化。", data); }), ); server.registerTool( "resource_export", { title: "导出 KOC 资源", description: "按账号、IP地区、合作来源或平台筛选并导出 KOC 资源 Excel。", inputSchema: z.object(resourceFilters), annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false }, }, withError(async ({ query, ip_location, cooperation_source, platform }) => { const data = await resourceExport({ query, ipLocation: ip_location, cooperationSource: cooperation_source, platform }, options.origin); return result(`已生成 ${data.account_count} 个账号的导出文件。`, data); }), ); }