#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; const UNITY_BRIDGE_URL = (process.env.UNITY_BRIDGE_URL || "http://127.0.0.1:19744").replace(/\/$/, ""); const server = new McpServer({ name: "vrbeats-unity", version: "0.1.0", }); function textResult(value) { const text = typeof value === "string" ? value : JSON.stringify(value, null, 2); return { content: [ { type: "text", text, }, ], }; } async function callUnity(path, options = {}) { const response = await fetch(`${UNITY_BRIDGE_URL}${path}`, { method: options.method || "GET", headers: { "Content-Type": "application/json", }, body: options.body === undefined ? undefined : JSON.stringify(options.body), }); const rawText = await response.text(); let parsed; try { parsed = rawText.length > 0 ? JSON.parse(rawText) : {}; } catch { parsed = { ok: false, rawText }; } if (!response.ok) { const detail = typeof parsed === "object" ? JSON.stringify(parsed, null, 2) : rawText; throw new Error(`Unity bridge HTTP ${response.status}: ${detail}`); } return parsed; } function queryString(params) { const query = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null && value !== "") { query.set(key, String(value)); } } const value = query.toString(); return value.length > 0 ? `?${value}` : ""; } const Vector3Schema = z.object({ x: z.number(), y: z.number(), z: z.number(), }); server.registerTool( "unity_health", { title: "Unity Bridge Health", description: "Check whether the Unity Editor bridge is reachable and report play state.", inputSchema: {}, }, async () => textResult(await callUnity("/health")), ); server.registerTool( "unity_capture_game_view", { title: "Capture Unity Game View", description: "Capture the active Unity camera to Captures/latest.png and return the local file path.", inputSchema: { width: z.number().int().min(320).max(4096).default(1280), height: z.number().int().min(180).max(4096).default(720), }, }, async ({ width = 1280, height = 720 }) => { const result = await callUnity(`/capture${queryString({ width, height })}`); return textResult(result); }, ); server.registerTool( "unity_get_console_logs", { title: "Get Unity Console Logs", description: "Read recent logs captured by the Unity Editor bridge.", inputSchema: { count: z.number().int().min(1).max(250).default(80), }, }, async ({ count = 80 }) => textResult(await callUnity(`/logs${queryString({ count })}`)), ); server.registerTool( "unity_list_scene_roots", { title: "List Unity Scene Roots", description: "List root GameObjects in the active Unity scene.", inputSchema: {}, }, async () => textResult(await callUnity("/scene/roots")), ); server.registerTool( "unity_list_scene_objects", { title: "List Unity Scene Objects", description: "List GameObjects in the active Unity scene, optionally filtered by hierarchy path text.", inputSchema: { query: z.string().optional().describe("Case-insensitive path filter."), limit: z.number().int().min(1).max(500).default(120), }, }, async ({ query = "", limit = 120 }) => textResult(await callUnity(`/scene/objects${queryString({ query, limit })}`)), ); server.registerTool( "unity_get_object", { title: "Get Unity Object", description: "Read transform and component information for a GameObject by hierarchy path.", inputSchema: { path: z.string().min(1).describe("Hierarchy path, for example Canvas/Panel/Button."), }, }, async ({ path }) => textResult(await callUnity(`/object${queryString({ path })}`)), ); server.registerTool( "unity_set_transform", { title: "Set Unity Object Transform", description: "Set world/local position, rotation, or local scale for a GameObject by hierarchy path.", inputSchema: { path: z.string().min(1), position: Vector3Schema.optional(), localPosition: Vector3Schema.optional(), rotationEuler: Vector3Schema.optional(), localRotationEuler: Vector3Schema.optional(), scale: Vector3Schema.optional(), }, }, async (input) => textResult(await callUnity("/transform", { method: "POST", body: input })), ); server.registerTool( "unity_set_play_state", { title: "Set Unity Play State", description: "Start, pause, resume, or stop Unity Play Mode.", inputSchema: { state: z.enum(["play", "pause", "resume", "stop"]), }, }, async ({ state }) => { const endpoint = state === "stop" ? "/stop" : state === "pause" ? "/pause" : "/play"; return textResult(await callUnity(endpoint)); }, ); const transport = new StdioServerTransport(); await server.connect(transport);