#!/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"; 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 ""; } })(); 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, 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, }), }; 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 { const result = await t.handler(args as Record, ctx); return json(result); } catch (e) { return fail(`${t.name}: ${(e as Error).message}`); } }, ); } await server.connect(new StdioServerTransport());