feat(redstone): wiki_recipe tool — extract recipe + thumbnail in one call
Adds a new MCP tool that parses minecraft.wiki's <table data-description=
"Crafting recipes"> directly, pulling the ingredients column out of each
row (e.g. "Block of Iron + Iron Ingot" for Anvil), then chases up the
page's main item thumbnail via prop=pageimages so the bot can deliver
text + a real image in one tool round.
Why a dedicated tool: minecraft.wiki does not host standalone "recipe.png"
files — recipes are rendered live as HTML grids composed from individual
BlockSprite icons. Filtering wiki_page_images for 'recipe' / 'craft' /
'grid' returned empty for every craftable item, leaving the model with
nothing to call tg_send_photo with. Confirmed by inspecting the Anvil
page's image inventory (Anvil1-6.png + GUI + sound files, no recipe).
Tool returns {
title, page_url,
recipes: [{ ingredients: "…" }, …], // strips <br>/<a>/ cleanly
thumbnail_url, // for tg_send_photo
note?, // present when no recipe rows
}
Persona prompt tightened:
- wiki_recipe is now the FIRST call for "how do I craft X" / "recipe for X"
- wiki_page_images is downgraded to "specific non-thumbnail images only"
- IMAGE DELIVERY MANDATORY clause: must end the turn with tg_send_photo
using a real URL or say plainly that no thumbnail was found — no more
describing-the-image-in-prose-and-stopping failures.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,76 @@ export const minecraftWikiTools: Tool<z.ZodRawShape>[] = [
|
||||
},
|
||||
}),
|
||||
|
||||
tool({
|
||||
name: "wiki_recipe",
|
||||
title: "Get a crafting recipe from the Minecraft wiki",
|
||||
description:
|
||||
"Extracts the crafting recipe ingredient list from a wiki page (parses the live recipe table — minecraft.wiki renders recipes as HTML grids, not standalone images, so wiki_page_images is the wrong tool for recipe queries). Returns the ingredients string for each recipe variant, plus the page's main item thumbnail URL to send with tg_send_photo. Use this whenever a user asks 'how do I craft X' or 'recipe for X'.",
|
||||
parameters: {
|
||||
title: z.string().min(1).describe("Exact wiki page title — e.g. 'Anvil', 'Beacon', 'Crafting Table'. Use wiki_search first if unsure."),
|
||||
},
|
||||
handler: async ({ title }) => {
|
||||
// 1. Parsed HTML — the recipe lives in a wikitable with data-description="Crafting recipes"
|
||||
type ParseRes = { parse?: { title?: string; text?: string } };
|
||||
const data = await wikiGet<ParseRes>({
|
||||
action: "parse",
|
||||
page: title,
|
||||
prop: "text",
|
||||
redirects: "1",
|
||||
disableeditsection: "1",
|
||||
});
|
||||
const html = data.parse?.text ?? "";
|
||||
if (!html) return { error: `no wiki page titled '${title}'` };
|
||||
const resolvedTitle = data.parse?.title ?? title;
|
||||
|
||||
// 2. Find each crafting-recipes table and pull the first <td> (ingredients) of each data row.
|
||||
const stripHtml = (s: string) =>
|
||||
s.replace(/<br\s*\/?>/gi, " + ")
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.replace(/ | /g, " ")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/\s*\+\s*/g, " + ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
const tableRe = /<table\b[^>]*data-description="Crafting recipes"[^>]*>([\s\S]*?)<\/table>/g;
|
||||
const recipes: { ingredients: string }[] = [];
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = tableRe.exec(html)) !== null) {
|
||||
const rowRe = /<tr\b[^>]*>([\s\S]*?)<\/tr>/g;
|
||||
let r: RegExpExecArray | null;
|
||||
while ((r = rowRe.exec(m[1])) !== null) {
|
||||
if (/<th\b/.test(r[1])) continue;
|
||||
const td = /<td\b[^>]*>([\s\S]*?)<\/td>/.exec(r[1]);
|
||||
if (!td) continue;
|
||||
const text = stripHtml(td[1]);
|
||||
if (text) recipes.push({ ingredients: text });
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Pull the page thumbnail in a second query — gives the bot a URL to send.
|
||||
type ThumbRes = { query?: { pages?: { title: string; thumbnail?: { source: string } }[] } };
|
||||
const meta = await wikiGet<ThumbRes>({
|
||||
action: "query",
|
||||
prop: "pageimages",
|
||||
titles: resolvedTitle,
|
||||
piprop: "thumbnail",
|
||||
pithumbsize: 600,
|
||||
redirects: "1",
|
||||
});
|
||||
const thumbnail = meta.query?.pages?.[0]?.thumbnail?.source ?? null;
|
||||
|
||||
return {
|
||||
title: resolvedTitle,
|
||||
recipes, // [{ ingredients: "Block of Iron + Iron Ingot" }, ...]
|
||||
thumbnail_url: thumbnail, // page's main item image — pass to tg_send_photo
|
||||
page_url: pageUrl(resolvedTitle),
|
||||
note: recipes.length === 0
|
||||
? "No recipe table on this page — the item may be uncraftable, found only via loot/structure, or listed under a different page."
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
tool({
|
||||
name: "wiki_page_images",
|
||||
title: "List images on a Minecraft wiki page (with full URLs)",
|
||||
|
||||
Reference in New Issue
Block a user