From f05db3990371af56353f15e763304608d1114de5 Mon Sep 17 00:00:00 2001 From: Paul Kloppers Date: Wed, 13 May 2026 23:29:45 +0200 Subject: [PATCH] feat(redstone): connect minecraft.wiki via three new MCP tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redstone can now query the official Minecraft wiki and deliver crafting recipe / item images straight into the chat. New: mcp/lib/minecraft-wiki-tools.ts exports three tools, all hitting https://minecraft.wiki/api.php (MediaWiki API, formatversion=2): - wiki_search(query, limit?) — opensearch top results, namespace 0, returns { title, description, url } for each hit. - wiki_page(title, thumb_size?) — extracts|pageimages → intro extract (plain text, ~600 chars) + thumbnail_url + page_url. - wiki_page_images(title, filter?, limit?) — generator=images + prop=imageinfo (iiprop=url|mime|size) returns every image embedded on the page along with its direct https URL. The `filter` substring is case-insensitive against the filename — pass 'recipe' / 'craft' / 'grid' to surface crafting-recipe diagrams. Bot wiring (bot/bot.ts): - import minecraftWikiTools, merge into allTools so it flows through the existing OpenRouter + Gemini function/tool schemas automatically. - Persona prompt teaches the model the canonical flow: wiki_search → wiki_page → wiki_page_images(filter='recipe') → tg_send_photo(url, caption). Composes with the already-existing tg_send_photo tool — no new telegram tool needed. User-Agent on every wiki request, 15s timeout, JSON-only. Co-Authored-By: Claude Opus 4.7 --- bot/bot.ts | 4 +- mcp/lib/minecraft-wiki-tools.ts | 146 ++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 mcp/lib/minecraft-wiki-tools.ts diff --git a/bot/bot.ts b/bot/bot.ts index 5d510ad..6b5605d 100644 --- a/bot/bot.ts +++ b/bot/bot.ts @@ -1059,6 +1059,7 @@ import { } from "/mc/mcp/lib/types.ts"; import { telegramTools } from "/mc/mcp/lib/telegram-tools.ts"; import { minecraftTools } from "/mc/mcp/lib/minecraft-tools.ts"; +import { minecraftWikiTools } from "/mc/mcp/lib/minecraft-wiki-tools.ts"; const tgClient = createTgClient(TOKEN); const mcRuntime = createMcRuntime({ @@ -1072,7 +1073,7 @@ const mcRuntime = createMcRuntime({ backupSh: `${MC_DIR}/backup.sh`, }); const toolCtx: ToolCtx = { tg: tgClient, db, mc: mcRuntime }; -const allTools = [...telegramTools, ...minecraftTools]; +const allTools = [...telegramTools, ...minecraftTools, ...minecraftWikiTools]; const toolsByName = new Map(allTools.map((t) => [t.name, t])); // Hidden from Redstone's view: emoji→sticker is handled by post-processing // the reply text, so the model never has to manage file_ids itself. @@ -1202,6 +1203,7 @@ function redstonePersona(user: User, chatType: string, chatId: number, srv: stri "- Telegram chat: send messages/polls/reactions/dice/stickers/photos, edit/pin/delete your own messages, query chat info (tg_*).", "- Minecraft server: check status (server_status), start/stop/restart services (server_up/down/restart), tail logs (server_logs), look up players in the bot's database (players_list/player_get).", "- Admin-only tools (the caller's role is shown in the context block): run RCON commands (rcon), trigger a world backup (backup), and modify player records (player_upsert/set_*/remove, seen_list). If a non-admin asks for one, politely refuse and don't call it.", + "- Minecraft wiki: look things up on minecraft.wiki and surface images into the chat. Usual flow for a 'how do I craft X?' / 'what is X?' question: wiki_search to pick the exact page title → wiki_page for a one-line summary + thumbnail → wiki_page_images with filter='recipe' or 'craft' or 'grid' to find a crafting-grid image, then tg_send_photo to send the image URL straight into the chat (caption it briefly).", "- When you use a tool, you usually don't need a separate text reply unless you're adding context — the tool's effect IS the response. After a server_status / players_list lookup, summarize the answer in one sentence.", "", "Stickers — drop emojis inline and the bot does the rest:", diff --git a/mcp/lib/minecraft-wiki-tools.ts b/mcp/lib/minecraft-wiki-tools.ts new file mode 100644 index 0000000..3edea9c --- /dev/null +++ b/mcp/lib/minecraft-wiki-tools.ts @@ -0,0 +1,146 @@ +// Minecraft Wiki tool catalog. Hits https://minecraft.wiki (MediaWiki API). +// Composes well with telegram-tools.ts → after wiki_page_images returns a URL +// the model can pass it to tg_send_photo to surface a recipe / item image +// directly into the chat. +import { z } from "zod"; +import { tool, type Tool } from "./types.ts"; + +const WIKI_BASE = "https://minecraft.wiki"; +const WIKI_API = `${WIKI_BASE}/api.php`; +const UA = "Redstone-bot/1.0 (Minecraft container; contact via telegram bot)"; +const TIMEOUT_MS = 15_000; + +async function wikiGet(params: Record): Promise { + const url = new URL(WIKI_API); + url.searchParams.set("format", "json"); + url.searchParams.set("formatversion", "2"); + for (const [k, v] of Object.entries(params)) { + if (v !== undefined) url.searchParams.set(k, String(v)); + } + const res = await fetch(url.toString(), { + headers: { "User-Agent": UA, Accept: "application/json" }, + signal: AbortSignal.timeout(TIMEOUT_MS), + }); + if (!res.ok) throw new Error(`wiki ${res.status}: ${(await res.text()).slice(0, 200)}`); + return (await res.json()) as T; +} + +function pageUrl(title: string): string { + return `${WIKI_BASE}/w/${encodeURIComponent(title.replace(/ /g, "_"))}`; +} + +export const minecraftWikiTools: Tool[] = [ + tool({ + name: "wiki_search", + title: "Search the Minecraft wiki", + description: + "Search minecraft.wiki for pages matching a query. Returns top matches with title, short description, and canonical page URL. Use this first to find the exact page title before calling wiki_page or wiki_page_images.", + parameters: { + query: z.string().min(1).describe("Search terms, e.g. 'iron pickaxe', 'crafting table', 'beacon'"), + limit: z.number().int().min(1).max(15).optional().describe("Max results (default 5)"), + }, + handler: async ({ query, limit }) => { + // opensearch returns a 4-tuple: [query, titles[], descriptions[], urls[]] + type OpenSearch = [string, string[], string[], string[]]; + const data = await wikiGet({ + action: "opensearch", + search: query, + limit: limit ?? 5, + namespace: 0, + }); + const [, titles, descriptions, urls] = data; + return { + results: titles.map((title, i) => ({ + title, + description: descriptions[i] || "", + url: urls[i] || pageUrl(title), + })), + }; + }, + }), + + tool({ + name: "wiki_page", + title: "Get a Minecraft wiki page summary", + description: + "Fetch the intro extract (plain text) plus the page's thumbnail image URL and canonical URL. Good for a one-line answer about an item, mob, or mechanic, plus a representative image to send.", + parameters: { + title: z.string().min(1).describe("Exact wiki page title (use wiki_search first if unsure)"), + thumb_size: z.number().int().min(120).max(1200).optional().describe("Thumbnail pixel width (default 600)"), + }, + handler: async ({ title, thumb_size }) => { + type PageRes = { + query?: { + pages?: { + title: string; + missing?: boolean; + extract?: string; + thumbnail?: { source: string; width: number; height: number }; + pageimage?: string; + }[]; + }; + }; + const data = await wikiGet({ + action: "query", + prop: "extracts|pageimages", + titles: title, + exintro: "1", + explaintext: "1", + exchars: 600, + piprop: "thumbnail|name", + pithumbsize: thumb_size ?? 600, + redirects: "1", + }); + const page = data.query?.pages?.[0]; + if (!page || page.missing) return { error: `no wiki page titled '${title}'` }; + return { + title: page.title, + extract: page.extract ?? "", + thumbnail_url: page.thumbnail?.source ?? null, + page_url: pageUrl(page.title), + }; + }, + }), + + tool({ + name: "wiki_page_images", + title: "List images on a Minecraft wiki page (with full URLs)", + description: + "Returns every image embedded on a wiki page along with its direct https URL. Use the optional `filter` to narrow by filename substring (case-insensitive) — pass 'recipe' or 'craft' or 'grid' to surface crafting-recipe diagrams. Hand a URL from this result to tg_send_photo to deliver the image into a chat.", + parameters: { + title: z.string().min(1).describe("Exact wiki page title"), + filter: z.string().optional().describe("Case-insensitive substring filter on the filename (e.g. 'recipe', 'craft', 'grid')"), + limit: z.number().int().min(1).max(50).optional().describe("Max images to return (default 20)"), + }, + handler: async ({ title, filter, limit }) => { + type ImgRes = { + query?: { + pages?: { + title: string; + imageinfo?: { url: string; mime?: string; width?: number; height?: number }[]; + }[]; + }; + }; + const data = await wikiGet({ + action: "query", + generator: "images", + titles: title, + prop: "imageinfo", + iiprop: "url|mime|size", + gimlimit: 50, + redirects: "1", + }); + const pages = data.query?.pages ?? []; + let images = pages.map((p) => ({ + filename: p.title.replace(/^File:/, ""), + url: p.imageinfo?.[0]?.url ?? "", + mime: p.imageinfo?.[0]?.mime, + })).filter((i) => i.url && (!i.mime || i.mime.startsWith("image/"))); + if (filter) { + const needle = filter.toLowerCase(); + images = images.filter((i) => i.filename.toLowerCase().includes(needle)); + } + return { images: images.slice(0, limit ?? 20) }; + }, + }), +];