endfield-mcp
Version:
MCP Server for Arknights: Endfield (TypeScript / stdio + Streamable HTTP)
219 lines • 10.8 kB
JavaScript
/**
* Endfield Wiki tool registrations — the 6-tool MVP surface.
*
* Tool names use the `ef_` prefix (project decision: short prefix, saves
* context budget vs. the long `endfield_` form). Descriptions are written
* in Chinese (the project's user-facing language); the underlying wiki
* content is English, which the calling LLM bridges as needed.
*
* Ported from PRTS-MCP's `ts/src/tools/prtsTools.ts`; the tool-call shapes
* are identical to keep migration intuition between the two projects.
*/
import { z } from "zod";
import { searchWiki, readPage, listSections, getCategories, getLinks, getTemplateData, } from "../api/endfieldWiki.js";
export function registerWikiTools(server) {
server.tool("ef_search_wiki", [
"搜索《明日方舟:终末地》英文 Wiki(endfield.wiki.gg)词条。",
"这是探索世界观设定(阵营、地点、敌人、机制)和查证专有名词英文名的第一步——先搜索拿到准确词条标题,再传给 ef_read_wiki_page 读全文。返回标题、简短摘要和匹配总数。",
"适用场景:用户想了解世界观/背景设定、需要某个名词的准确英文写法、或要找某个角色/敌人/物品的 Wiki 页面时。Wiki 内容主要是英文,返回结果也是英文,必要时请翻译后再使用。",
].join(" "), {
query: z
.string()
.describe("搜索关键词,建议用英文以获得最佳匹配,如「Endfield」、「Angelic」、角色英文名。"),
limit: z
.number()
.int()
.min(1)
.max(20)
.default(5)
.describe("返回结果数量上限,默认 5,最大建议不超过 10。"),
search_mode: z
.enum(["text", "title"])
.default("text")
.describe("搜索模式:text(全文搜索,默认)或 title(仅搜索标题)。"),
filter_technical: z
.boolean()
.default(true)
.describe("是否过滤 Template/Module/Widget 等技术命名空间页面,默认 true。"),
}, async ({ query, limit, search_mode, filter_technical }) => {
let result;
try {
result = await searchWiki(query, limit, search_mode, filter_technical);
}
catch (e) {
return {
content: [
{ type: "text", text: e instanceof Error ? e.message : String(e) },
],
};
}
if (result.results.length === 0) {
return {
content: [{ type: "text", text: `No pages matched '${query}'.` }],
};
}
const header = `# Search "${query}" (${result.totalHits} matches)\n`;
const body = result.results
.map((r) => `**${r.title}**\n${r.snippet}`)
.join("\n\n---\n\n");
return { content: [{ type: "text", text: header + body }] };
});
server.tool("ef_read_wiki_page", [
"读取 endfield.wiki.gg 指定词条的纯文本内容。",
"返回清洗后的纯文本(已去除 CSS、HTML 标签和实体),内容可能较长。强烈建议先调用 ef_list_wiki_sections 查看目录结构,再用 section_index 按需读取特定章节,避免整页内容过载;不填 section_index 返回整页。",
"适用场景:已通过 ef_search_wiki 拿到准确标题后,需要该词条的完整正文时使用。如果不确定标题,先用 ef_search_wiki 搜索。",
].join(" "), {
page_title: z
.string()
.describe("词条标题,需与 Wiki 页面标题完全一致(英文,区分大小写),如「Endfield」、「Angelic」。建议通过 ef_search_wiki 获取准确标题后再传入。"),
section_index: z
.number()
.int()
.optional()
.describe("可选章节编号(从 ef_list_wiki_sections 获取)。不填则返回整页内容;填入编号如 1 则仅返回该节。"),
}, async ({ page_title, section_index }) => {
const text = await readPage(page_title, section_index);
return { content: [{ type: "text", text }] };
});
server.tool("ef_list_wiki_sections", [
"列出 endfield.wiki.gg 页面的目录(章节列表)。",
"返回章节编号、层级和标题。编号以 T- 开头表示该节来自模板嵌入(如角色信息框)。拿到编号后传给 ef_read_wiki_page 的 section_index 按需读取特定章节。",
"适用场景:词条内容较长、只想读某一节(如「背景设定」「游戏数据」),或想先了解页面结构再决定读哪部分时使用。比直接整页读取更省上下文。",
].join(" "), {
page_title: z
.string()
.describe("词条标题,需与 Wiki 页面标题完全一致,如「Endfield」。"),
}, async ({ page_title }) => {
try {
const sections = await listSections(page_title);
if (sections.length === 0) {
return {
content: [
{ type: "text", text: `Page '${page_title}' has no sections.` },
],
};
}
const lines = sections.map((s) => `[${s.index}] L${s.level} ${s.line}`);
return { content: [{ type: "text", text: lines.join("\n") }] };
}
catch (e) {
return {
content: [
{ type: "text", text: e instanceof Error ? e.message : String(e) },
],
};
}
});
server.tool("ef_get_wiki_categories", [
"获取 endfield.wiki.gg 页面的分类标签。",
"返回该页面所属的所有分类(英文),如「Characters」「Weapons」「Items」,可用于判断页面类型和所属体系。",
"适用场景:需要确认一个词条的归属类别(是角色/敌人/物品/地点),或想顺着分类发现同类相关页面时使用。",
].join(" "), {
page_title: z
.string()
.describe("词条标题,如「Endfield」、角色英文名等。"),
}, async ({ page_title }) => {
try {
const cats = await getCategories(page_title);
if (cats.length === 0) {
return {
content: [
{ type: "text", text: `Page '${page_title}' has no categories.` },
],
};
}
return {
content: [
{ type: "text", text: cats.map((c) => `- ${c}`).join("\n") },
],
};
}
catch (e) {
return {
content: [
{ type: "text", text: e instanceof Error ? e.message : String(e) },
],
};
}
});
server.tool("ef_get_wiki_links", [
"获取 endfield.wiki.gg 页面的相关链接(知识图谱关系)。",
"outbound 返回该页面引用的其他词条链接;inbound 返回反向链接(哪些页面引用了它)。可用于发现相关角色、阵营或剧情线索。",
"适用场景:想围绕某个词条展开、找它的关联实体(如某角色出场的所有章节、引用某阵营的所有角色),或做世界观关系梳理时使用。",
].join(" "), {
page_title: z.string().describe("词条标题,如「Endfield」。"),
direction: z
.enum(["outbound", "inbound"])
.default("outbound")
.describe("链接方向:outbound(页面引用的链接,默认)或 inbound(引用该页面的链接)。"),
limit: z
.number()
.int()
.min(1)
.max(100)
.default(30)
.describe("返回链接数量上限,默认 30。"),
}, async ({ page_title, direction, limit }) => {
try {
const result = await getLinks(page_title, direction, limit);
if (result.links.length === 0) {
return {
content: [
{
type: "text",
text: `Page '${page_title}' has no ${direction} links.`,
},
],
};
}
const header = `# ${direction} links for '${page_title}' (${result.total} total${result.hasMore ? ", truncated" : ""})\n`;
const body = result.links.map((l) => `- ${l}`).join("\n");
return { content: [{ type: "text", text: header + body }] };
}
catch (e) {
return {
content: [
{ type: "text", text: e instanceof Error ? e.message : String(e) },
],
};
}
});
server.tool("ef_get_wiki_template", [
"提取 endfield.wiki.gg 页面中的结构化模板数据(infobox 字段)。",
"解析页面源码里的 {{Template|key=value|...}} 调用,返回按模板名分组的键值对字典,如角色信息框里的属性、阵营、稀有度等数值字段。模板名和键名为英文(Wiki 源码原样),值可能是英文或数字。",
"适用场景:需要某个实体的结构化数值/属性(而非阅读正文叙述),或要对比多个角色的 infobox 字段时使用。",
].join(" "), {
page_title: z
.string()
.describe("词条标题,如角色英文名。建议先用 ef_search_wiki 确认标题。"),
}, async ({ page_title }) => {
try {
const templates = await getTemplateData(page_title);
const names = Object.keys(templates);
if (names.length === 0) {
return {
content: [
{
type: "text",
text: `Page '${page_title}' has no template data.`,
},
],
};
}
const parts = names.map((name) => {
const fields = templates[name];
const lines = Object.entries(fields).map(([k, v]) => ` ${k}: ${typeof v === "object" ? JSON.stringify(v) : String(v)}`);
return `**${name}**\n${lines.join("\n")}`;
});
return { content: [{ type: "text", text: parts.join("\n\n") }] };
}
catch (e) {
return {
content: [
{ type: "text", text: e instanceof Error ? e.message : String(e) },
],
};
}
});
}
//# sourceMappingURL=wikiTools.js.map