Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 265 additions & 0 deletions docs/MCP.md

Large diffs are not rendered by default.

201 changes: 201 additions & 0 deletions docs/MCP_SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# MCP 安全与威胁模型

本文定义 CUE MCP 的安全边界、默认策略和上线检查。MCP 拥有与游戏内调试器近似的权限;启用任意反射写入或方法调用后,应按“可在当前游戏进程中执行高权限操作”的接口对待,而不是普通遥测 API。

## 1. 资产与信任边界

需要保护的资产:

- 游戏进程的完整性、稳定性和可恢复状态;
- 存档、配置、截图及游戏可访问的本地文件;
- 用户账号、多人会话和反作弊状态;
- MCP token、对象数据、日志及可能包含隐私的运行时字符串;
- 主机 CPU、内存、磁盘和 Unity 主线程帧预算。

信任边界:

1. 不受信任的自然语言、网页、游戏文本和模型输出进入 Agent。
2. MCP 客户端通过回环网络直接连接 CUE DLL 内置的 SSE 或 Streamable HTTP Server。
3. DLL 内置网络层完成会话管理、鉴权、协议解析、大小限制和请求排队。
4. 网络线程把已验证请求交给 Unity 主线程。
5. 反射/Unity API 穿过托管、IL2CPP 和原生对象边界。
6. 截图、导出、存档或日志穿过游戏进程到文件系统。

本方案不依赖 Node.js、stdio 适配器或其他外部转发组件;鉴权和权限检查全部由 DLL 内置服务和游戏内 Runtime 执行器完成。

**不可信输入包括 Agent 自己生成的参数。** Prompt injection 可能来自对象名称、组件字符串、游戏聊天、网页或用户提供的脚本。服务端不能因为调用来自“受信任 Agent”就跳过 schema、权限和范围校验。

## 2. 安全目标

- 默认不可远程访问、默认拒绝未鉴权请求。
- 默认只读、默认关闭危险操作,重启后不保留危险授权。
- 所有 Unity 操作在主线程执行,并受超时、队列及每帧预算控制。
- 工具采用最小权限和显式 allowlist;未知工具、类型、成员或参数默认拒绝。
- 写操作可审计、可验证;高影响操作需用户逐次批准。
- token、敏感对象数据和文件内容不进入普通日志。
- 客户端断开或超时不会产生无限重试和重复副作用。

## 3. 基线安全策略(MUST)

### 网络与鉴权

- DLL 内置 MCP Server **MUST** 只监听 `127.0.0.1`,可选监听 `::1`;禁止 `0.0.0.0` 和非回环网卡。
- RPC **MUST** 要求高熵 token。token 比较应避免明显的时序泄漏,并在失败时返回统一错误。
- **MUST** 限制 HTTP 方法、`Content-Type`、`Accept`、请求体字节数、JSON 深度、字符串长度和响应体大小。
- SSE 会话 **MUST** 有连接数、空闲时间、写入队列和生命周期上限;断开后必须释放会话及网络资源。
- Streamable HTTP 可采用无状态模式;此时不得伪造或宣称存在 `Mcp-Session-Id`。若未来启用有状态会话,则会话标识和协议版本头必须按协议校验,并拒绝伪造、过期或跨连接复用。
- UI 切换 SSE/Streamable HTTP 后必须重启 MCP Server,使旧监听和旧会话失效;不能让两种模式意外共享未验证的会话状态。
- **MUST NOT** 接受 URL 查询参数中的 token;**MUST NOT** 在日志打印鉴权头。
- 浏览器可访问的 HTTP 实现 **MUST NOT** 开放宽泛 CORS;对意外的 `Origin` 头应默认拒绝,避免恶意网页利用本机端口。
- 健康端点不得泄露工具、对象、token 或详细异常;可配置为也要求 token。

### 权限

- 启动默认 `readOnly=true`、`allowDangerous=false`。
- 只读模式必须由服务端强制执行,不能只靠 UI 隐藏按钮或依赖客户端描述。
- 每个工具必须带服务端权限分类;未知分类按危险操作拒绝。
- 危险操作开关必须在 UI 中明确可见,并在每次进程启动时复位为关闭。
- 反射成员、可实例化类型、可读写路径和文件导出目录必须支持 denylist/allowlist;禁止绕过 CUE 已有反射黑名单。

### 执行与资源控制

- 网络线程不得直接访问 UnityEngine 对象。
- 请求队列必须有上限;满时快速失败,不可无限缓存。
- 每帧执行请求数和单请求工作量必须有上限。
- 枚举、对象图序列化和递归必须限制深度、节点数、集合项数及字符串长度。
- 异常必须转换为结构化错误;默认不向客户端暴露完整本地路径、栈、token 或私有字段值。
- 非幂等操作应支持调用 ID/去重窗口,或明确告诉客户端不可在超时后自动重试。

## 4. 操作分级

| 级别 | 示例 | 默认 | 用户确认 |
|---|---|---|---|
| R0 诊断 | ping、版本、Bridge 状态、安全模式 | 允许 | 不需要 |
| R1 只读 | 场景/对象/组件列表,安全字段读取 | 只读模式允许 | 批量/敏感数据可要求 |
| W1 可恢复写入 | Transform、启停对象、简单数值字段 | 默认拒绝 | 每个任务明确确认并写后验证 |
| D1 破坏性 | 销毁对象、移除组件、卸载/加载场景、静态状态写入 | 危险开关关闭 | 必须逐次确认 |
| D2 代码/外部副作用 | 任意方法调用、类型实例化、文件读写、网络、进程/原生接口 | 应默认永久禁用或严格 allowlist | 双重确认;能不用则不用 |

名称不是分类依据。例如 `get_Current()` 是方法,可能有副作用;`saveScreenshot` 会写文件;`set_active(false)` 可能中断关键系统。分类必须绑定到实际执行器和成员策略。

### 4.1 当前运行时权限矩阵

| Runtime 操作 | `MCP_Read_Only=true` | 只读关闭、危险关闭 | 只读关闭、危险开启 |
|---|---:|---:|---:|
| `status`、`list_scenes`、`search`、`snapshot`、`get_member` | 允许 | 允许 | 允许 |
| `set_member`、`set_transform`、`set_enabled` | 拒绝 | 允许 | 允许 |
| `invoke`、`create`、`destroy` | 拒绝 | 拒绝 | 允许 |
| `batch` | 每个子命令分别检查 | 每个子命令分别检查 | 每个子命令分别检查 |

权限必须在游戏内 Runtime 执行器中强制,而不是依赖 MCP tool annotation、Trae/其他客户端的审批设置或 UI 是否显示某个按钮。通用入口 `game.execute` 与批处理也必须先解析实际内部命令,再应用相同策略。

### 4.2 批处理非原子边界

批处理减少网络往返,但不是安全事务。当前应假设:

- 子操作按顺序执行,已成功的前序修改不会因后续失败而回滚;
- `stop_on_error=true` 只阻止尚未执行的后续项;
- `atomic=true` 只是能力请求,Bridge 可拒绝,客户端不能据此承诺回滚;
- Unity 方法、对象创建/销毁、场景行为和原生副作用通常不可逆;
- 客户端超时时操作可能已经在主线程执行,因此危险批次不得自动重试。

服务端必须对每个子操作单独鉴权、分类和限流,拒绝递归批处理。Agent 应在执行前展示逐项计划,执行后逐项读取验证,并明确标注部分成功状态。

## 5. 主要威胁与缓解

| 威胁 | 典型场景 | 影响 | 必需缓解 |
|---|---|---|---|
| 未授权本机访问 | 恶意进程扫描回环端口 | 任意修改、数据泄露 | token、回环绑定、短暴露周期、轮换 |
| 局域网/公网暴露 | 绑定 `0.0.0.0` 或端口转发 | 远程接管游戏 | 拒绝非回环绑定;仅使用认证加密隧道 |
| 浏览器对 localhost 的攻击 | 恶意网页 POST 到已知端口 | CSRF 式调用 | token 自定义头、拒绝 CORS/Origin、JSON Content-Type |
| Prompt injection | 游戏聊天/对象名诱导 Agent 调危险工具 | 越权或破坏状态 | 把运行时文本视为数据;服务端权限;用户确认 |
| token 泄露 | 配置提交、日志、截图或命令行 | 会话接管 | `.gitignore`、日志脱敏、header 传递、快速轮换 |
| 参数混淆/对象替换 | 场景切换后旧 ID 指向无效对象 | 修改错误目标 | 句柄带会话/场景世代;写前重读名称、类型、路径 |
| 反射副作用 | getter、`ToString()`、方法执行游戏逻辑 | 保存损坏、网络行为、崩溃 | allowlist;只读模式拒绝隐式代码执行 |
| 拒绝服务 | 深对象图、大集合、昂贵 getter、请求洪泛 | 掉帧、OOM、卡死 | 大小/深度/队列/每帧限制,取消与熔断 |
| 超时重放 | 客户端没收到响应后重试销毁/创建 | 重复副作用 | 幂等键/去重;写后读取;禁止自动重试 |
| 路径穿越 | 导出工具接收 `../` 或绝对路径 | 覆盖/泄露文件 | 固定导出根目录、规范化后验证、拒绝链接和绝对路径 |
| 类型混淆 | JSON 数字/枚举/重载错误转换 | 错误调用或内存问题 | 严格 schema、范围校验、完整签名和显式转换 |
| IL2CPP/原生边界失效 | 已销毁对象包装仍非 null | 崩溃或未定义行为 | Unity 存活检查、异常隔离、保守 API 集 |
| 信息泄露 | 错误返回栈、本地路径或私有状态 | 隐私与后续攻击 | 面向客户端的精简错误;详细日志本地且脱敏 |

## 6. 危险操作确认建议

仅有一个全局复选框不足以表达用户意图。推荐流程:

1. Agent 提交只读 `plan/preview`,列出工具、目标、旧值、新值和预计影响。
2. UI 为该计划生成短期、单次确认 nonce;nonce 绑定会话、工具、参数摘要和过期时间。
3. 实际危险调用必须携带 nonce;参数变化、过期或重复使用均拒绝。
4. 执行后返回每个目标的结果,并通过只读查询验证。
5. 对可恢复字段保留会话内 undo 记录;不要声称可以撤销方法调用或场景副作用。

如果当前实现尚无 nonce,至少要求 MCP UI 中危险开关 + 客户端逐次人工审批,并将危险模式的开启时间保持尽可能短。

## 7. 日志与审计

建议审计字段:时间、会话 ID、请求 ID、客户端标识、工具名、权限级别、目标的非敏感标识、结果码、耗时和是否被策略拒绝。

不得记录:完整 token/鉴权头、任意文件内容、完整对象转储、账号凭据、聊天隐私、未脱敏路径或超长参数。参数审计应采用允许字段摘要;token 最多显示不可逆指纹或末尾极少字符,且后者也非必需。

日志应有轮换、大小和保留期限。UI 中“复制诊断信息”必须自动遮盖 token。

## 8. 发布前安全测试

### 自动化协议测试

- SSE 与 Streamable HTTP 分别测试无 token、错误 token、正确 token;
- SSE 测试事件流握手、endpoint/message 事件、心跳、客户端断开、无效会话和并发连接上限;
- Streamable HTTP 测试 `GET`/`POST`/通知、`Accept`、`Content-Type` 和协议版本;无状态模式验证不强制 session header,有状态模式再测试会话 ID 及无效/过期会话;
- 畸形 JSON、重复字段、过深 JSON、超大请求、超大响应、队列满和超时;
- JSON-RPC 缺少 `jsonrpc`/`id`/`method`,未知方法,notification;
- 响应 ID 与请求一致,错误结构稳定,网络日志不泄露 token;
- 在 MCP UI 切换传输模式并重启后,旧连接与旧会话不可继续使用。

### 权限测试

- 只读模式逐个拒绝所有 W1/D1/D2 工具;
- getter、`ToString()`、静态成员、索引器、事件和委托不可通过只读路径旁路;
- 危险关闭时销毁、移除组件、场景操作、方法调用和文件访问均失败;
- 关闭/重启 Bridge 后危险授权失效;
- `tools/list` 不发布当前模式下不可安全调用的能力,或工具调用时可靠拒绝。

### 网络测试

```powershell
Get-NetTCPConnection -State Listen | Where-Object LocalPort -eq 17891
```

确认 `LocalAddress` 仅为 `127.0.0.1` 或 `::1`。还应从同局域网另一台机器确认不能连接。不要仅依赖 Windows 防火墙弥补错误绑定。

### 稳定性测试

- 加载/卸载场景时并发查询;
- 对象在排队后、执行前被销毁;
- 游戏暂停、低帧率和长时间主线程卡顿;
- Mono 与 IL2CPP 分别测试值类型、枚举、数组、泛型和重载;
- 10k+ 对象场景和大型集合的分页/截断;
- 客户端超时、断开、重连后不重复执行危险请求。

## 9. 安全部署检查表

- [ ] DLL 内置 MCP Server 仅监听回环地址。
- [ ] RPC token 已生成,长度足够,未提交版本库。
- [ ] 只读默认开启,危险默认关闭,重启后复位。
- [ ] MCP 客户端对写入/危险工具启用人工审批。
- [ ] 请求、响应、JSON 深度、队列和每帧预算均有限制。
- [ ] 反射和文件能力使用 allowlist,沿用 CUE 黑名单。
- [ ] SSE/Streamable HTTP 网络日志已脱敏,不记录鉴权头、token 或完整敏感载荷。
- [ ] SSE 与 Streamable HTTP 均完成 `initialize`、`tools/list` 和只读调用 smoke test。
- [ ] Mono 和目标 IL2CPP 游戏均完成负面权限及稳定性测试。
- [ ] 已准备 token 轮换、停止 DLL 内置 MCP Server、终止活动会话和恢复存档的方法。

## 10. 事件响应

发现异常调用、token 泄露或游戏状态被意外修改时:

1. 立即在 MCP UI 停止 DLL 内置 MCP Server,断开全部 SSE/Streamable HTTP 会话。
2. 保存脱敏后的请求 ID、时间和错误日志,不保存 token。
3. 轮换 token,并检查端口是否曾绑定到非回环地址。
4. 从可信存档/配置恢复;不要假定反射修改可以自动撤销。
5. 检查 MCP 客户端会话中的 prompt injection 和自动审批规则。
6. 在确认根因和策略修复前,仅以只读模式重新启用。
80 changes: 79 additions & 1 deletion src/Config/ConfigManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using UnityExplorer.UI;
using UnityExplorer.MCP.Transport;
using UnityExplorer.UI;
using UnityExplorer.UI.Panels;

namespace UnityExplorer.Config
Expand Down Expand Up @@ -35,6 +36,23 @@ public static class ConfigManager
public static ConfigElement<float> Arrow_Size;
public static ConfigElement<bool> Freecam_Camera_Target_Selection;

// MCP server settings
public static ConfigElement<bool> MCP_Enabled;
public static ConfigElement<McpTransportMode> MCP_Transport_Mode;
public static ConfigElement<string> MCP_Bind_Address;
public static ConfigElement<int> MCP_Port;
public static ConfigElement<string> MCP_Rpc_Path;
public static ConfigElement<string> MCP_Health_Path;
public static ConfigElement<string> MCP_Auth_Token;
public static ConfigElement<bool> MCP_Require_Token_For_Health;
public static ConfigElement<bool> MCP_Read_Only;
public static ConfigElement<bool> MCP_Allow_Dangerous_Operations;
public static ConfigElement<bool> MCP_Request_Logging;
public static ConfigElement<int> MCP_Request_Timeout_Milliseconds;
public static ConfigElement<int> MCP_Max_Request_Body_Bytes;
public static ConfigElement<int> MCP_Max_Pending_Requests;
public static ConfigElement<int> MCP_Max_Requests_Per_Frame;

public static ConfigElement<KeyCode> Pause;
public static ConfigElement<KeyCode> Frameskip;
public static ConfigElement<KeyCode> Screenshot;
Expand Down Expand Up @@ -204,6 +222,66 @@ private static void CreateConfigElements()
"Enables certain advanced settings on the Freecam panel, in case the user can't get the freecam to work properly (requires game reset).",
false);

MCP_Enabled = new("MCP Enabled",
"Start the local MCP server after CinematicUnityExplorer finishes initializing.",
false);

MCP_Transport_Mode = new("MCP Transport Mode",
"MCP protocol transport hosted directly by the CinematicUnityExplorer DLL. Supported values are SSE and StreamableHTTP. Restart MCP after changing this value.",
McpTransportMode.SSE);

MCP_Bind_Address = new("MCP Bind Address",
"Address exposed by the MCP transport. The built-in transport currently supports loopback only.",
"127.0.0.1");

MCP_Port = new("MCP Port",
"TCP port used by the local MCP HTTP bridge. Restart MCP after changing this value.",
17891);

MCP_Rpc_Path = new("MCP RPC Path",
"HTTP path used for JSON-RPC requests. Restart MCP after changing this value.",
"/mcp");

MCP_Health_Path = new("MCP Health Path",
"HTTP path used for MCP health checks. Restart MCP after changing this value.",
"/health");

MCP_Auth_Token = new("MCP Auth Token",
"Bearer token required by the MCP HTTP bridge. A secure token is generated automatically when MCP starts if this value is empty.",
"");

MCP_Require_Token_For_Health = new("MCP Require Token For Health",
"Require the configured MCP bearer token for health-check requests.",
false);

MCP_Read_Only = new("MCP Read Only",
"Block mutation tools and permit inspection-only MCP operations.",
true);

MCP_Allow_Dangerous_Operations = new("MCP Allow Dangerous Operations",
"Allow high-risk tools such as object destruction, arbitrary invocation, and scene changes.",
false);

MCP_Request_Logging = new("MCP Request Logging",
"Retain a small in-memory log of MCP requests for the MCP UI.",
false);

MCP_Request_Timeout_Milliseconds = new("MCP Request Timeout Milliseconds",
"Maximum time an HTTP request waits for Unity main-thread execution.",
30000);

MCP_Max_Request_Body_Bytes = new("MCP Max Request Body Bytes",
"Maximum accepted JSON-RPC request body size.",
1024 * 1024);

MCP_Max_Pending_Requests = new("MCP Max Pending Requests",
"Maximum number of requests waiting for Unity main-thread execution.",
128);

MCP_Max_Requests_Per_Frame = new("MCP Max Requests Per Frame",
"Maximum number of queued MCP requests executed during one Unity Update.",
16);

Pause = new("Pause",
"Toggle the pause of the game.",
KeyCode.PageUp);
Expand Down
4 changes: 4 additions & 0 deletions src/ExplorerBehaviour.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using UnityExplorer.Config;
using UnityExplorer.MCP;
using UnityExplorer.UI;
using UnityExplorer.UI.Panels;
using UnityExplorer.UI.Widgets;
Expand Down Expand Up @@ -38,6 +39,7 @@ internal static void Setup()
internal void Update()
{
ExplorerCore.Update();
McpManager.PumpMainThread();
}

// For editor, to clean up objects
Expand All @@ -53,6 +55,8 @@ internal void OnApplicationQuit()
{
if (quitting) return;
quitting = true;
McpManager.Shutdown();

if (UIManager.UIRoot)
TryDestroy(UIManager.UIRoot.transform.root.gameObject);

Expand Down
2 changes: 2 additions & 0 deletions src/ExplorerCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
global using UniverseLib.Utility;
using UnityExplorer.CatmullRom;
using UnityExplorer.Config;
using UnityExplorer.MCP;
using UnityExplorer.ObjectExplorer;
using UnityExplorer.Runtime;
using UnityExplorer.UI;
Expand Down Expand Up @@ -67,6 +68,7 @@ public static void Init(IExplorerLoader loader)
static void LateInit()
{
SceneHandler.Init();
McpManager.Initialize();

Log($"Creating UI...");

Expand Down
Loading