// Shared types + helpers used by both telegram-tools and minecraft-tools. import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema"; import type { Database } from "bun:sqlite"; // ---- Telegram HTTP client ---- export type TgClient = { call: (method: string, params?: Record) => Promise; }; export function createTgClient(token: string): TgClient { if (!token) throw new Error("createTgClient: empty token"); const base = `https://api.telegram.org/bot${token}`; return { async call(method: string, params: Record = {}): Promise { const res = await fetch(`${base}/${method}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(params), signal: AbortSignal.timeout(20_000), }); const data = (await res.json()) as { ok: boolean; result?: T; description?: string; error_code?: number }; if (!data.ok) throw new Error(`${method}: ${data.description} (code ${data.error_code})`); return data.result as T; }, }; } // ---- Minecraft runtime (docker/host shell) ---- export type ShResult = { ok: boolean; code: number; out: string; err: string }; export type McConfig = { mcDir: string; composeFile: string; composeProject: string; serverSvc: string; serverContainer: string; pfSvc: string; pfContainer: string; backupSh: string; }; export type McRuntime = { config: McConfig; sh: (cmd: string[]) => Promise; compose: (...args: string[]) => Promise; containerRunning: (name: string) => Promise; }; export function createMcRuntime(config: McConfig): McRuntime { async function sh(cmd: string[]): Promise { const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" }); const [out, err] = await Promise.all([ new Response(proc.stdout).text(), new Response(proc.stderr).text(), ]); const code = await proc.exited; return { ok: code === 0, code, out: out.trimEnd(), err: err.trimEnd() }; } return { config, sh, compose: (...args) => sh([ "docker", "compose", "--project-directory", config.mcDir, "-p", config.composeProject, "-f", config.composeFile, ...args, ]), async containerRunning(name) { const r = await sh(["docker", "inspect", "-f", "{{.State.Running}}", name]); return r.ok && r.out === "true"; }, }; } // ---- Tool definition shape ---- export type ToolCtx = { tg: TgClient; db: Database; mc: McRuntime; }; export type Tool = { name: string; title: string; description: string; parameters: Params; handler: (args: z.infer>, ctx: ToolCtx) => Promise; /** Bot-side: only admins may invoke. MCP server ignores this (caller is trusted). */ requiresAdmin?: boolean; }; // Strip per-tool shape so a heterogeneous catalog type-checks. export function tool

(t: Tool

): Tool { return t as Tool; } // ---- Zod -> Gemini function declaration ---- function cleanForGemini(s: unknown): unknown { if (Array.isArray(s)) return s.map(cleanForGemini); if (s && typeof s === "object") { const out: Record = {}; for (const [k, v] of Object.entries(s as Record)) { if (k === "$schema" || k === "additionalProperties" || k === "default") continue; // Gemini's schema dialect uses minimum/maximum only, not the exclusive variants. if (k === "exclusiveMinimum" || k === "exclusiveMaximum") continue; if (k === "anyOf" || k === "oneOf") { const variants = (v as unknown[]).filter((x) => x && (x as Record).type !== "null"); if (variants.length === 1) return cleanForGemini(variants[0]); return { type: "string" }; } out[k] = cleanForGemini(v); } return out; } return s; } export function toolToGeminiFunction(t: Tool) { const schema = zodToJsonSchema(z.object(t.parameters), { $refStrategy: "none", target: "openApi3", }); const params = cleanForGemini(schema) as Record; return { name: t.name, description: t.description, parameters: params.type ? params : { type: "object", properties: {} }, }; }