在本地把 Chatbox + MCP 集成进 Unity

环境:Windows 11,Node.js 22,Unity(Editor),Chatbox 本地部署

最终效果是

  • 在 Chatbox 里输入指令,让 MCP 调用工具

  • 工具经由本地 Node MCP Server 转发到 Unity

  • Unity Editor 收到命令,在编辑器里执行静态方法,例如打印日志等等

    图片

    图片


1. 整体架构

1
2
3
4
5
6
7
8
9
10
11
Chatbox (MCP Client)

│ STDIO (MCP 协议)

Node.js MCP Server (unity-local)

│ HTTP (本地回环)

Unity Editor
├─ McpUnityBridge (Editor 脚本,启动 HTTP 监听)
└─ UnityMCPTools (静态类,承载具体功能)
  • Chatbox:作为 MCP 客户端,通过 STDIO 启动 MCP 服务器,并调用工具。
  • Node MCP Server:本地 server,暴露 MCP 工具 unity_pingunity_log 等,把调用转成 HTTP 请求发给 Unity。
  • Unity Editor:打开项目时自动启动一个本地 HTTP Bridge,接受 MCP Server 的 /command 请求,然后在主线程调用静态类 UnityMCPTools 里的方法。

2. 准备 Node 工程:unity-mcp-server

2.1 初始化项目

在本地创建 MCP Server 目录:

1
2
3
4
mkdir D:\tools\unity-mcp-server
cd D:\tools\unity-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk

然后在 package.json 里加上 ESM 标记和 CLI 声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "unity-mcp-server",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"bin": {
"unity-mcp-server": "./server.mjs"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.21.0"
}
}
  • type: "module":使用 ES Module 语法(import)。
  • bin.unity-mcp-server:以后可以通过 unity-mcp-server 作为命令行入口。

2.2 server.mjs:Node 侧 MCP Server

D:\tools\unity-mcp-server 下创建 server.mjs,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

const UNITY_BRIDGE_URL = process.env.UNITY_BRIDGE_URL ?? 'http://127.0.0.1:58888';

class UnityMcpServer {
constructor() {
console.error('[Unity MCP] Starting server.mjs');
console.error('[Unity MCP] UNITY_BRIDGE_URL =', UNITY_BRIDGE_URL);

this.server = new Server(
{ name: 'unity-local', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
this.setupHandlers();
}

setupHandlers() {
// 列出可用工具
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'unity_ping',
description: '调用 UnityMCPTools.Ping()',
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
},
{
name: 'unity_log',
description: '调用 UnityMCPTools.Log(message)',
inputSchema: {
type: 'object',
properties: {
message: { type: 'string' },
},
required: ['message'],
additionalProperties: false,
},
},
],
}));

// 工具调用入口
this.server.setRequestHandler(CallToolRequestSchema, async (req) => {
const { name, arguments: args = {} } = req.params;

console.error('[Unity MCP] CallTool:', name, 'args =', args);

if (name === 'unity_ping') {
return await this.callUnityMethod('Ping', {});
}

if (name === 'unity_log') {
return await this.callUnityMethod('Log', { message: args.message });
}

return {
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
};
});
}

async callUnityMethod(method, payload) {
try {
console.error('[Unity MCP] POST -> Unity:', method, payload);
const res = await fetch(`${UNITY_BRIDGE_URL}/command`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ method, ...payload }),
});

const text = await res.text();
console.error('[Unity MCP] Unity response:', text);
return {
content: [{ type: 'text', text }],
};
} catch (e) {
console.error('[Unity MCP] Failed to call Unity:', e);
return {
content: [
{
type: 'text',
text: `Failed to call Unity method "${method}": ${e}`,
},
],
};
}
}

async run() {
console.error('[Unity MCP] Connecting to MCP client over stdio...');
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('[Unity MCP] Connected. Waiting for requests...');
process.stdin.resume();
}
}

const server = new UnityMcpServer();
server.run().catch((err) => {
console.error('[Unity MCP] Fatal error:', err);
process.exit(1);
});

这里有几个关键点:

  • 使用 StdioServerTransport,通过 stdin/stdout 与 Chatbox 通讯。
  • 暴露两个工具:unity_pingunity_log
  • 所有调用都会转成 HTTP POST 到 Unity 本地 Bridge 的 /command 接口。

手动测试时,可以直接在目录下执行:

1
node .\server.mjs

正常情况下会打印几行初始化日志,然后进程“挂住”——这是在等 MCP 客户端发请求,属于正常状态。


3. Unity 侧:桥接与静态工具

Unity 这边的目标有两个:

  1. 打开项目时自动启动一个本地 HTTP 服务作为 Bridge。
  2. 把 HTTP body 里的 JSON 映射成对静态类 UnityMCPTools 的方法调用。

3.1 MCPCommand & UnityMCPTools:静态工具类

先创建一个脚本 Assets/Editor/MCP/UnityMCPTools.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using UnityEngine;

[System.Serializable]
public class MCPCommand
{
public string method; // 要调用的方法名,例如 "Ping" / "Log"
public string message; // 示例参数,用于 Log
}

public static class UnityMCPTools
{
// 统一入口:根据 method 路由到具体静态方法
public static string HandleCommand(MCPCommand cmd)
{
switch (cmd.method)
{
case "Ping":
return Ping();

case "Log":
return Log(cmd.message);

default:
var unknown = "[MCP] Unknown method: " + cmd.method;
Debug.LogWarning(unknown);
return unknown;
}
}

// ===== 真正暴露给 MCP 的功能 =====

public static string Ping()
{
var msg = "[MCP] UnityMCPTools.Ping() at " + System.DateTime.Now;
Debug.Log(msg);
return msg;
}

public static string Log(string message)
{
var msg = "[MCP] UnityMCPTools.Log(): " + message;
Debug.Log(msg);
return msg;
}

// 后续可以继续扩展:
// public static string CreateScript(string name, string content) { ... }
// 在 HandleCommand 里加 case "CreateScript": ...
}

这里的设计是:

  • MCP 端只需要传一个 JSON:{"method":"Log","message":"hello"}
  • Unity 解析成 MCPCommand,然后由 UnityMCPTools.HandleCommand 路由到具体方法。

3.2 Unity Bridge:Editor 启动时自动监听 HTTP

再创建 Assets/Editor/MCP/UnityMCPBridge.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
using UnityEditor;
using UnityEngine;
using System.Net;
using System.Text;
using System.Threading;
using System.Collections.Generic;

[InitializeOnLoad]
public static class UnityMCPBridge
{
private static HttpListener listener;
private static Thread listenerThread;
private const string Url = "http://127.0.0.1:58888/";

// 用队列把后台线程收到的命令丢到主线程处理
private static readonly Queue<string> commandQueue = new Queue<string>();

static UnityMCPBridge()
{
StartServer();
EditorApplication.update += OnEditorUpdate;
EditorApplication.quitting += OnQuit;
}

private static void StartServer()
{
if (listener != null) return;

try
{
listener = new HttpListener();
listener.Prefixes.Add(Url);
listener.Start();

listenerThread = new Thread(ListenLoop);
listenerThread.IsBackground = true;
listenerThread.Start();

Debug.Log("[MCP] Unity bridge listening at " + Url);
}
catch (System.Exception e)
{
Debug.LogError("[MCP] Failed to start HttpListener: " + e);
}
}

private static void ListenLoop()
{
while (listener != null && listener.IsListening)
{
HttpListenerContext ctx = null;
try
{
ctx = listener.GetContext();
HandleRequest(ctx);
}
catch
{
try { ctx?.Response.Abort(); } catch { }
}
}
}

private static void HandleRequest(HttpListenerContext ctx)
{
var path = ctx.Request.Url.AbsolutePath;

// 健康检查
if (path == "/ping")
{
var bytes = Encoding.UTF8.GetBytes("Unity bridge OK");
ctx.Response.OutputStream.Write(bytes, 0, bytes.Length);
ctx.Response.Close();
return;
}

// MCP 调用统一走 /command,body 是 JSON:{ "method": "...", ... }
if (path == "/command" && ctx.Request.HttpMethod == "POST")
{
string body;
using (var reader = new System.IO.StreamReader(ctx.Request.InputStream, ctx.Request.ContentEncoding))
{
body = reader.ReadToEnd();
}

lock (commandQueue)
{
commandQueue.Enqueue(body);
}

var bytes = Encoding.UTF8.GetBytes("Unity accepted command.");
ctx.Response.OutputStream.Write(bytes, 0, bytes.Length);
ctx.Response.Close();
return;
}

ctx.Response.StatusCode = 404;
ctx.Response.Close();
}

// 主线程中执行 MCPCommand -> UnityMCPTools
private static void OnEditorUpdate()
{
while (true)
{
string body = null;
lock (commandQueue)
{
if (commandQueue.Count == 0) break;
body = commandQueue.Dequeue();
}

if (!string.IsNullOrEmpty(body))
{
try
{
var cmd = JsonUtility.FromJson<MCPCommand>(body);
var result = UnityMCPTools.HandleCommand(cmd);
}
catch (System.Exception e)
{
Debug.LogError("[MCP] Failed to handle command: " + e);
}
}
}
}

private static void OnQuit()
{
try
{
if (listener != null)
{
listener.Stop();
listener.Close();
listener = null;
}
}
catch { }
}
}

要点:

  • 使用 [InitializeOnLoad],只要打开这个 Unity 工程,编辑器就自动启动 HTTP 监听。
  • 使用 commandQueue 把后台线程收到的命令交给主线程处理,避免在非主线程使用 Unity API。
  • /ping 用于简单健康检查; /command 用于 MCP 调用。

此时只要打开 Unity 工程,在 Console 里应该能看到:

1
[MCP] Unity bridge listening at http://127.0.0.1:58888/

可以用 curl 测一下:

1
2
curl http://127.0.0.1:58888/ping
# 期待输出:Unity bridge OK

4. Chatbox 端:配置 MCP Server

Chatbox 作为 MCP 客户端,需要在配置里加入一个 server,指向刚刚写的 server.mjs

关键点只有两条:

  1. 使用绝对路径,不要用相对路径,否则会出现类似:

    1
    Cannot find module 'D:\tools\Chatbox\toolsunity-mcp-serverserver.mjs'
  2. Windows 下在 JSON 里写路径要用 \\ 转义。

MCP 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"mcpServers": {
"unity-local": {
"command": "node",
"args": [
"D:\\tools\\unity-mcp-server\\server.mjs"
],
"env": {
"UNITY_BRIDGE_URL": "http://127.0.0.1:58888"
},
"description": "Unity local MCP server",
"capabilities": {
"tools": {}
}
}
}
}

之后重启 Chatbox,在对应会话中启用 unity-local 这个 MCP server,就可以从 Chatbox 里调用 unity_pingunity_log 工具。