UNPKG

endfield-mcp

Version:

MCP Server for Arknights: Endfield (TypeScript / stdio + Streamable HTTP)

149 lines • 8.35 kB
/** * Story tool registrations — dialogue scenes, chapters, search. * * Four `ef_` tools over the story bundle, mirroring PRTS-MCP's core * story tool subset. These are the **fan-creation core** for * worldbuilding — writers use them to read in-game dialogue, find * specific scenes by keyword, and browse the chapter structure. * * - ef_list_story_chapters → browse chapter structure (main story, side, character) * - ef_list_stories → list scenes within a chapter * - ef_read_story → read one full dialogue scene * - ef_search_stories → full-text search across all scenes */ import { z } from "zod"; import { hasStoryData, listStories as _listStories, listStoryChapters as _listStoryChapters, readStory as _readStory, searchStories as _searchStories, } from "../data/story.js"; import { withGracefulError } from "./toolRuntime.js"; function storyNotAvailable() { return { content: [ { type: "text", text: "剧情数据暂不可用。story bundle 可能尚未同步——请稍候(后台 sync 进行中)或检查网络连接。", }, ], }; } export function registerStoryTools(server) { server.tool("ef_list_story_chapters", [ "列出《明日方舟:终末地》的剧情章节结构(浏览剧情目录)。", "返回所有章节(主线 e1-e10、支线 sm、角色故事 c、活动 a 等)及其场景数量和显示名称。拿到章节 ID 后传给 ef_list_stories 查看该章下的所有对话场景。", "适用场景:想系统阅读某段剧情、浏览全部章节范围,或定位某个故事属于哪条线时使用。这是探索剧情的第一步。若已知关键词要直接找片段,用 ef_search_stories 更快。", ].join(" "), {}, withGracefulError("story bundle", async () => { if (!hasStoryData()) return storyNotAvailable(); const chapters = _listStoryChapters(); if (chapters.length === 0) { return { content: [{ type: "text", text: "未找到任何剧情章节。" }] }; } const header = `# 剧情章节(共 ${chapters.length} 个)\n`; const body = chapters .map((c) => `- **${c.chapterId}** ${c.displayName}(${c.entryCount} 个场景)`) .join("\n"); return { content: [{ type: "text", text: header + body }] }; })); server.tool("ef_list_stories", [ "列出指定章节下的所有对话场景。", "传入章节 ID(从 ef_list_story_chapters 获取,如「e1」「sm1」「c6」),返回该章下所有场景的 key、所属任务、场景号、行数和预览文本。拿到场景 key 后传给 ef_read_story 阅读完整对话。", "适用场景:已选定某章节、想看这一章里有哪些具体场景(按任务/场景号排列),从中挑出要细读的那段时使用。", ].join(" "), { chapter_id: z .string() .describe("章节 ID,如「e1」(主线第一章)、「sm1」(支线)、「c6」(6 号角色的故事)。从 ef_list_story_chapters 获取。"), }, withGracefulError("story bundle", async ({ chapter_id }) => { if (!hasStoryData()) return storyNotAvailable(); const stories = _listStories(chapter_id); if (stories.length === 0) { return { content: [ { type: "text", text: `未找到章节「${chapter_id}」下的场景。请用 ef_list_story_chapters 查看所有可用章节 ID。`, }, ], }; } const header = `# 章节「${chapter_id}」(${stories.length} 个场景)\n`; const body = stories .map((s) => `- **${s.key}** | 任务 ${s.mission} 场景${s.scene} | ${s.lineCount}行 ${s.actorCount}角色 | ${s.preview.slice(0, 60)}${s.preview.length > 60 ? "..." : ""}`) .join("\n"); return { content: [{ type: "text", text: header + body }] }; })); server.tool("ef_read_story", [ "读取一个对话场景的完整台词(阅读剧情文本的核心工具)。", "返回该场景的所有对话行(角色名:台词)、旁白(*斜体*)、玩家选项(如果有)。需要场景 key(从 ef_list_stories 或 ef_search_stories 获取,如「black_a1m6d2_3」)。", "适用场景:已定位到某个场景、想读它的完整对话原文时使用。可设 include_narration=false 只保留台词、跳过场景描写。若还不知场景 key,先用 ef_list_stories 或 ef_search_stories 查找。", ].join(" "), { conv_key: z .string() .describe("场景 key(conv 文件名),如「black_a1m6d2_3」「dlg_map02_lv003_env_2」。从 ef_list_stories 或 ef_search_stories 获取。"), include_narration: z .boolean() .default(true) .describe("是否包含旁白和场景描述,默认 true。设为 false 只保留对话台词。"), }, withGracefulError("story bundle", async ({ conv_key, include_narration }) => { if (!hasStoryData()) return storyNotAvailable(); const scene = _readStory(conv_key); if (scene === null) { return { content: [ { type: "text", text: `未找到场景「${conv_key}」。请确认 key 正确(从 ef_list_stories 或 ef_search_stories 获取)。`, }, ], }; } const header = `# ${scene.missionName} · 场景${scene.scene}(${scene.lines.length} 行)\n`; const bodyParts = []; for (const line of scene.lines) { if (line.type === "dialog") { bodyParts.push(`${line.role ?? "(未知)"}:${line.text}`); } else if (line.type === "narration" && include_narration) { bodyParts.push(`*${line.text}*`); } } if (scene.choices.length > 0) { bodyParts.push("\n【玩家选项】"); for (const c of scene.choices) { bodyParts.push(` ${c.index}. ${c.text}`); } } return { content: [{ type: "text", text: header + bodyParts.join("\n") }] }; })); server.tool("ef_search_stories", [ "在所有剧情对话中执行正则全文搜索(跨剧情找片段)。", "用于按关键词、角色名、地点、事件等查找剧情片段,如「源石」「陈千语」「塔卫二」。返回匹配的场景 key、所属任务和预览片段。", "适用场景:记得某段剧情的关键词但不知在哪一章、想找某角色出场的所有片段、或要搜集某个设定/事件的所有提及时使用。拿到 key 后用 ef_read_story 读全文。若想系统浏览章节用 ef_list_story_chapters。", ].join(" "), { pattern: z .string() .max(200, "搜索模式过长(上限 200 字符),请缩短后重试。") .describe("正则表达式(大小写不敏感,上限 200 字符),如「源石」「陈千语」「基地」。无效正则会退化为字面子串匹配。"), max_results: z .number() .int() .min(1) .max(100) .default(30) .describe("返回结果数量上限,默认 30。"), }, withGracefulError("story bundle", async ({ pattern, max_results }) => { if (!hasStoryData()) return storyNotAvailable(); const results = _searchStories(pattern, max_results); if (results.length === 0) { return { content: [{ type: "text", text: `未找到匹配「${pattern}」的剧情片段。` }], }; } const header = `# 搜索「${pattern}」(${results.length} 条匹配)\n`; const body = results .map((r) => `**${r.key}** | ${r.entry.mission} | ${r.entry.lineCount}行\n${r.snippet}`) .join("\n\n---\n\n"); return { content: [{ type: "text", text: header + body }] }; })); } //# sourceMappingURL=storyTools.js.map