feat: polish VR gameplay and sync tools
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
# VRBeats Unity MCP Server
|
||||
|
||||
This MCP server connects Codex to the Unity Editor bridge in this project.
|
||||
|
||||
## Unity side
|
||||
|
||||
Open the project in Unity. The bridge auto-starts on domain reload and listens only on:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:19744
|
||||
```
|
||||
|
||||
Manual controls are available in Unity:
|
||||
|
||||
```text
|
||||
Tools > Codex Bridge > Start Server
|
||||
Tools > Codex Bridge > Stop Server
|
||||
Tools > Codex Bridge > Auto Start
|
||||
Tools > Codex Bridge > Capture Game View
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
Run once from this folder:
|
||||
|
||||
```powershell
|
||||
npm install
|
||||
```
|
||||
|
||||
## Codex MCP config
|
||||
|
||||
Add a server entry like this to the Codex MCP config, then restart Codex/VS Code so the tool list refreshes:
|
||||
|
||||
```toml
|
||||
[mcp_servers.vrbeats_unity]
|
||||
command = "node"
|
||||
args = ["C:\\Users\\User-40\\Desktop\\unity\\work\\BeatSabar\\VRBeatSaber\\tools\\unity-mcp-server\\index.mjs"]
|
||||
startup_timeout_sec = 30
|
||||
|
||||
[mcp_servers.vrbeats_unity.env]
|
||||
UNITY_BRIDGE_URL = "http://127.0.0.1:19744"
|
||||
```
|
||||
|
||||
## Exposed tools
|
||||
|
||||
- `unity_health`
|
||||
- `unity_capture_game_view`
|
||||
- `unity_get_console_logs`
|
||||
- `unity_list_scene_roots`
|
||||
- `unity_list_scene_objects`
|
||||
- `unity_get_object`
|
||||
- `unity_set_transform`
|
||||
- `unity_set_play_state`
|
||||
|
||||
`unity_capture_game_view` writes `Captures/latest.png` in the project root.
|
||||
@@ -0,0 +1,7 @@
|
||||
[mcp_servers.vrbeats_unity]
|
||||
command = "node"
|
||||
args = ["C:\\Users\\User-40\\Desktop\\unity\\work\\BeatSabar\\VRBeatSaber\\tools\\unity-mcp-server\\index.mjs"]
|
||||
startup_timeout_sec = 30
|
||||
|
||||
[mcp_servers.vrbeats_unity.env]
|
||||
UNITY_BRIDGE_URL = "http://127.0.0.1:19744"
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/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);
|
||||
Generated
+1162
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "vrbeats-unity-mcp-server",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"vrbeats-unity-mcp": "./index.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||
"zod": "^3.25.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node ./index.mjs"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user