Files

72 lines
2.7 KiB
TypeScript
Raw Permalink Normal View History

#!/usr/bin/env bun
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { Database } from "bun:sqlite";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { readFileSync } from "node:fs";
extract Minecraft tools to shared toolkit; gate Redstone replies to authorized senders Redstone previously only saw the 23 tg_* tools — it had no idea the Minecraft stack existed, so questions like "is the server up?" went unanswered. This change extracts the 15 Minecraft tools (lifecycle, rcon, backup, players, seen) into the same shared catalog the Telegram tools already used, so both Gemini and external MCP clients see them. mcp/lib/types.ts (new) holds the shared shape: Tool<P>, ToolCtx, createTgClient, createMcRuntime (a Bun.spawn-based wrapper for docker compose / docker exec), and toolToGeminiFunction (zod → Gemini schema, now also stripping exclusiveMinimum/Maximum since Gemini rejects them). mcp/lib/minecraft-tools.ts (new) is the catalog itself. Eight handlers are flagged requiresAdmin: rcon, backup, and all destructive player_* writes plus seen_list. mcp/server.ts trusts the caller (Claude / Paul on the host) and ignores the flag; bot/bot.ts honours it at dispatch time, returning {error: "...requires admin role..."} to Gemini so it can explain to the user instead of attempting the call. mcp/server.ts shrinks from 423 lines to 70 — a single loop over both catalogs replaces the hand-rolled registrations. bot/bot.ts wires both catalogs into the function declarations and adds the admin gate. It also gains a defensive re-check on every incoming group/DM text: the Redstone handler now does its own lookup of ctx.from.id against the users table and refuses to reply unless status='active'. This is belt-and-braces — the auth middleware already short-circuits unauthorized callers earlier in the chain, but with privacy-mode-off groups now feeding every message through, a future refactor that reorders middleware shouldn't be able to make Redstone respond to strangers. Drops thinkingConfig from the Gemini call (gemini-2.5-flash-lite rejects it outright with HTTP 400). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:19:27 +02:00
import { createTgClient, createMcRuntime, type ToolCtx } from "./lib/types.ts";
import { telegramTools } from "./lib/telegram-tools.ts";
import { minecraftTools } from "./lib/minecraft-tools.ts";
const HERE = dirname(fileURLToPath(import.meta.url));
const MC_DIR = process.env.MC_DIR ?? resolve(HERE, "..");
const COMPOSE_PROJECT = process.env.COMPOSE_PROJECT_NAME ?? "minecraft";
const COMPOSE_FILE = process.env.COMPOSE_FILE ?? `${MC_DIR}/docker-compose.yml`;
const SERVER_SVC = process.env.SERVER_SERVICE ?? "minecraft";
const SERVER_CONTAINER = process.env.SERVER_CONTAINER ?? "minecraft";
const PF_SVC = process.env.PORTFORWARD_SERVICE ?? "autossh";
const PF_CONTAINER = process.env.PORTFORWARD_CONTAINER ?? "minecraft-autossh";
const BACKUP_SH = process.env.BACKUP_SH ?? `${MC_DIR}/backup.sh`;
const DB_PATH = process.env.DB_PATH ?? `${MC_DIR}/bot/data/users.db`;
const db = new Database(DB_PATH);
db.exec("PRAGMA journal_mode = WAL");
const TG_TOKEN = (() => {
if (process.env.BOT_TOKEN) return process.env.BOT_TOKEN.trim();
const file = process.env.BOT_TOKEN_FILE ?? `${MC_DIR}/bot.token`;
try { return readFileSync(file, "utf8").trim(); } catch { return ""; }
})();
extract Minecraft tools to shared toolkit; gate Redstone replies to authorized senders Redstone previously only saw the 23 tg_* tools — it had no idea the Minecraft stack existed, so questions like "is the server up?" went unanswered. This change extracts the 15 Minecraft tools (lifecycle, rcon, backup, players, seen) into the same shared catalog the Telegram tools already used, so both Gemini and external MCP clients see them. mcp/lib/types.ts (new) holds the shared shape: Tool<P>, ToolCtx, createTgClient, createMcRuntime (a Bun.spawn-based wrapper for docker compose / docker exec), and toolToGeminiFunction (zod → Gemini schema, now also stripping exclusiveMinimum/Maximum since Gemini rejects them). mcp/lib/minecraft-tools.ts (new) is the catalog itself. Eight handlers are flagged requiresAdmin: rcon, backup, and all destructive player_* writes plus seen_list. mcp/server.ts trusts the caller (Claude / Paul on the host) and ignores the flag; bot/bot.ts honours it at dispatch time, returning {error: "...requires admin role..."} to Gemini so it can explain to the user instead of attempting the call. mcp/server.ts shrinks from 423 lines to 70 — a single loop over both catalogs replaces the hand-rolled registrations. bot/bot.ts wires both catalogs into the function declarations and adds the admin gate. It also gains a defensive re-check on every incoming group/DM text: the Redstone handler now does its own lookup of ctx.from.id against the users table and refuses to reply unless status='active'. This is belt-and-braces — the auth middleware already short-circuits unauthorized callers earlier in the chain, but with privacy-mode-off groups now feeding every message through, a future refactor that reorders middleware shouldn't be able to make Redstone respond to strangers. Drops thinkingConfig from the Gemini call (gemini-2.5-flash-lite rejects it outright with HTTP 400). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:19:27 +02:00
const ctx: ToolCtx = {
tg: TG_TOKEN ? createTgClient(TG_TOKEN) : {
call: async () => { throw new Error("No bot token (set BOT_TOKEN or BOT_TOKEN_FILE)."); },
},
db,
extract Minecraft tools to shared toolkit; gate Redstone replies to authorized senders Redstone previously only saw the 23 tg_* tools — it had no idea the Minecraft stack existed, so questions like "is the server up?" went unanswered. This change extracts the 15 Minecraft tools (lifecycle, rcon, backup, players, seen) into the same shared catalog the Telegram tools already used, so both Gemini and external MCP clients see them. mcp/lib/types.ts (new) holds the shared shape: Tool<P>, ToolCtx, createTgClient, createMcRuntime (a Bun.spawn-based wrapper for docker compose / docker exec), and toolToGeminiFunction (zod → Gemini schema, now also stripping exclusiveMinimum/Maximum since Gemini rejects them). mcp/lib/minecraft-tools.ts (new) is the catalog itself. Eight handlers are flagged requiresAdmin: rcon, backup, and all destructive player_* writes plus seen_list. mcp/server.ts trusts the caller (Claude / Paul on the host) and ignores the flag; bot/bot.ts honours it at dispatch time, returning {error: "...requires admin role..."} to Gemini so it can explain to the user instead of attempting the call. mcp/server.ts shrinks from 423 lines to 70 — a single loop over both catalogs replaces the hand-rolled registrations. bot/bot.ts wires both catalogs into the function declarations and adds the admin gate. It also gains a defensive re-check on every incoming group/DM text: the Redstone handler now does its own lookup of ctx.from.id against the users table and refuses to reply unless status='active'. This is belt-and-braces — the auth middleware already short-circuits unauthorized callers earlier in the chain, but with privacy-mode-off groups now feeding every message through, a future refactor that reorders middleware shouldn't be able to make Redstone respond to strangers. Drops thinkingConfig from the Gemini call (gemini-2.5-flash-lite rejects it outright with HTTP 400). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:19:27 +02:00
mc: createMcRuntime({
mcDir: MC_DIR,
composeFile: COMPOSE_FILE,
composeProject: COMPOSE_PROJECT,
serverSvc: SERVER_SVC,
serverContainer: SERVER_CONTAINER,
pfSvc: PF_SVC,
pfContainer: PF_CONTAINER,
backupSh: BACKUP_SH,
}),
};
extract Minecraft tools to shared toolkit; gate Redstone replies to authorized senders Redstone previously only saw the 23 tg_* tools — it had no idea the Minecraft stack existed, so questions like "is the server up?" went unanswered. This change extracts the 15 Minecraft tools (lifecycle, rcon, backup, players, seen) into the same shared catalog the Telegram tools already used, so both Gemini and external MCP clients see them. mcp/lib/types.ts (new) holds the shared shape: Tool<P>, ToolCtx, createTgClient, createMcRuntime (a Bun.spawn-based wrapper for docker compose / docker exec), and toolToGeminiFunction (zod → Gemini schema, now also stripping exclusiveMinimum/Maximum since Gemini rejects them). mcp/lib/minecraft-tools.ts (new) is the catalog itself. Eight handlers are flagged requiresAdmin: rcon, backup, and all destructive player_* writes plus seen_list. mcp/server.ts trusts the caller (Claude / Paul on the host) and ignores the flag; bot/bot.ts honours it at dispatch time, returning {error: "...requires admin role..."} to Gemini so it can explain to the user instead of attempting the call. mcp/server.ts shrinks from 423 lines to 70 — a single loop over both catalogs replaces the hand-rolled registrations. bot/bot.ts wires both catalogs into the function declarations and adds the admin gate. It also gains a defensive re-check on every incoming group/DM text: the Redstone handler now does its own lookup of ctx.from.id against the users table and refuses to reply unless status='active'. This is belt-and-braces — the auth middleware already short-circuits unauthorized callers earlier in the chain, but with privacy-mode-off groups now feeding every message through, a future refactor that reorders middleware shouldn't be able to make Redstone respond to strangers. Drops thinkingConfig from the Gemini call (gemini-2.5-flash-lite rejects it outright with HTTP 400). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:19:27 +02:00
const server = new McpServer({ name: "minecraft", version: "0.2.0" });
function ok(text: string) { return { content: [{ type: "text" as const, text }] }; }
function fail(text: string) { return { content: [{ type: "text" as const, text }], isError: true }; }
function json(obj: unknown) { return ok(JSON.stringify(obj, null, 2)); }
for (const t of [...minecraftTools, ...telegramTools]) {
server.registerTool(
t.name,
{ title: t.title, description: t.description, inputSchema: t.parameters },
async (args) => {
try {
extract Minecraft tools to shared toolkit; gate Redstone replies to authorized senders Redstone previously only saw the 23 tg_* tools — it had no idea the Minecraft stack existed, so questions like "is the server up?" went unanswered. This change extracts the 15 Minecraft tools (lifecycle, rcon, backup, players, seen) into the same shared catalog the Telegram tools already used, so both Gemini and external MCP clients see them. mcp/lib/types.ts (new) holds the shared shape: Tool<P>, ToolCtx, createTgClient, createMcRuntime (a Bun.spawn-based wrapper for docker compose / docker exec), and toolToGeminiFunction (zod → Gemini schema, now also stripping exclusiveMinimum/Maximum since Gemini rejects them). mcp/lib/minecraft-tools.ts (new) is the catalog itself. Eight handlers are flagged requiresAdmin: rcon, backup, and all destructive player_* writes plus seen_list. mcp/server.ts trusts the caller (Claude / Paul on the host) and ignores the flag; bot/bot.ts honours it at dispatch time, returning {error: "...requires admin role..."} to Gemini so it can explain to the user instead of attempting the call. mcp/server.ts shrinks from 423 lines to 70 — a single loop over both catalogs replaces the hand-rolled registrations. bot/bot.ts wires both catalogs into the function declarations and adds the admin gate. It also gains a defensive re-check on every incoming group/DM text: the Redstone handler now does its own lookup of ctx.from.id against the users table and refuses to reply unless status='active'. This is belt-and-braces — the auth middleware already short-circuits unauthorized callers earlier in the chain, but with privacy-mode-off groups now feeding every message through, a future refactor that reorders middleware shouldn't be able to make Redstone respond to strangers. Drops thinkingConfig from the Gemini call (gemini-2.5-flash-lite rejects it outright with HTTP 400). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 23:19:27 +02:00
const result = await t.handler(args as Record<string, unknown>, ctx);
return json(result);
} catch (e) {
return fail(`${t.name}: ${(e as Error).message}`);
}
},
);
}
await server.connect(new StdioServerTransport());