name: vrchat-unity-mcp description: VRChat Unity MCP connection via Cloudflare Tunnel — connection, protocol, and environment inspection
VRChat Unity MCP 连接与环境检查
架构
- Windows: Unity Editor (2022.3.22f1) + mcp-for-unity 插件 → 本地 HTTP 端口 8188
- 隧道:
cloudflared tunnel --url http://localhost:8188在 Windows 上运行 - Linux: 通过 Cloudflare Tunnel URL 访问(URL 每次重启都变)
- 本项目路径:
E:/ALCOM/project/Cazalis
连接步骤
1. 测试隧道是否可达
curl -s -o /dev/null -w "%{http_code}" https://<tunnel-url>.trycloudflare.com/health
# 返回 200 = 通
# 返回 503/502 = 隧道断开或服务未启动
# 返回 404 = 服务在但路径不对
2. 初始化 MCP Session
MCP-over-HTTP 需要先 initialize 拿到 session ID,之后所有请求带 session header:
# 获取 session ID (从 response headers 中的 mcp-session-id)
curl -s -D - "https://<tunnel>.trycloudflare.com/mcp" \
-X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"hermes","version":"1.0"}}}' \
| grep -i "mcp-session-id"
3. 设置活动实例
curl -s "https://<tunnel>.trycloudflare.com/mcp" \
-X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Session-Id: <session_id>" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"set_active_instance","arguments":{"instance":"Cazalis@<hash>"}}}'
MCP 协议关键点
params":{"name":"set_active_instance","arguments":{"instance":"Cazalis@
可用资源列表(通过 resources/list 获取):
- mcpforunity://instances — 列出 Unity 实例
- mcpforunity://project/info — 项目信息
- mcpforunity://editor/state — Editor 状态
- mcpforunity://custom-tools — 项目自定义工具
- mcpforunity://tool-groups — 工具组信息
- mcpforunity://project/layers / project/tags
- mcpforunity://scene/cameras
- mcpforunity://scene/gameobject/{id} — 单个 GameObject 详情
- mcpforunity://editor/windows — 打开的窗口
- mcpforunity://editor/selection — 当前选中
- mcpforunity://editor/state — 编译/Play Mode 状态
- mcpforunity://menu-items — 所有菜单项
- mcpforunity://rendering/stats
- mcpforunity://pipeline/renderer-features
工具调用 (tools/call)
返回值在 SSE data: 事件中。工具返回的 content[0].text 是 JSON string,需要二次 parse。
工具组管理
默认只启用 core 组。其他组(animation, docs, probuilder, vfx, ui, scripting_ext, testing)需要手动激活:
环境检查清单(存档用)
进行改模操作前应记录以下状态:
基础连接
- MCP health endpoint 返回 200
- 能获取到 Unity 实例列表
- 能设置活动实例 存档用)
进行改模操作前应记录以下状态:
基础连接
- MCP health endpoint 返回 200
- 能获取到 Unity 实例列表
- 能设置活动实例### Editor 状态
- 项目路径、Unity 版本
- 当前场景名称和路径
- Play Mode 是否运行
- 是否在编译中
- Console 有无报错
场景结构
- 根节点列表(层级)
- 主模型的完整子节点树(名称、InstanceID、活跃状态、组件类型)
- 备份模型的状态
资产清单
- AnimatorController 数量和路径
- VRCExpressionsMenu 数量和路径
- VRCExpressionParameters 数量和路径
- 溶解动画文件(ON/OFF 配对)
- 内衣材质路径
已有功能
- 已存在的 Toggle 功能(哪些能正常工作)
- 已损坏的功能(记录在案)
SSE 响应解析
响应格式为 Server-Sent Events:
需要取最后一个data: 块作为实际结果。中间可能有 notifications/message 事件。
}
event: message data: {...result...}
需要取最后一个 `data:` 块作为实际结果。中间可能有 `notifications/message` 事件。## Python MCP Client (SSE via requests)
When using `execute_code` (Python) to call MCP tools through Cloudflare Tunnel, `urllib` fails on second request (HTTP 406) because it can't handle SSE streaming. Use `requests` library instead:
```python
import requests, json
accept = "application/json, text/event-stream" # MUST include BOTH types!
base = "https://<tunnel>.trycloudflare.com/mcp"
def init():
h = {"Content-Type": "application/json", "Accept": accept}
b = json.dumps({"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2024-11-05","capabilities":{},
"clientInfo":{"name":"hermes","version":"1.0"}}})
r = requests.post(base, data=b, headers=h, stream=True, timeout=15)
sid = r.headers.get('mcp-session-id')
for _ in r.iter_lines(decode_unicode=True): pass # consume SSE
r.close()
return sid
def mcp_call(sid, name, args, id=2):
h = {"Content-Type":"application/json","Accept":accept,"MCP-Session-Id":sid}
b = json.dumps({"jsonrpc":"2.0","id":id,"method":"tools/call","params":{"name":name,"arguments":args}})
r = requests.post(base, data=b, headers=h, stream=True, timeout=30)
data_events = []
for line in r.iter_lines(decode_unicode=True):
if line and line.startswith('data:'):
data_events.append(line[5:].strip())
r.close()
if data_events:
return json.loads(data_events[-1])
return {"error":"no data"}
``Key:Acceptheader MUST be"application/json, text/event-stream"(both types). Session expires every ~30-60 seconds.
MUST be"application/json, text/event-stream"(both types). Session expires every ~30-60 seconds.## Known Issues
- Session 会超时,长流程需每次重新 initialize
-manage_gameobject的 action 只支持 CRUD(create/modify/delete/duplicate/move_relative/look_at),不支持 get_components
-manage_material的 action 不支持get_properties-manage_asset的get_components只对 GameObject 有效,ScriptableObject 会报错
-find_in_file和get_sha只支持.cs` 脚本文件
Material 操作指南
关键参数名(必须用对,否则报错)
| 操作 | 工具 | 关键参数 | 示例值 |
|---|---|---|---|
| 复制材质 | manage_asset action=duplicate |
path + destination |
"destination": "Assets/.../NewMat.mat" |
| 设贴图属性 | manage_material action=set_material_shader_property |
material_path, property, value |
"property": "_MainTex", "value": "Assets/.../tex.png" |
| 设数值属性 | 同上 | property, value |
"property": "_UseMatCap", "value": 1.0 |
| 赋材质到模型 | manage_material action=assign_material_to_renderer |
material_path, target, search_method |
"target": "Cazalis zigai/Body", "search_method": "by_path" |
注意事项
set_material_shader_property的value支持 string(贴图路径)、number(float/int)、boolassign_material_to_renderer的target接受 GameObject 路径、名称或实例ID,需配合search_method(by_path/by_name/by_id)- 贴图类型属性传值用项目相对路径(
Assets/.../file.png),不需要加property_type - 所有材质属性名全以小写
_开头(如_MainTex,_ShadowBorderMask) - lilToon 的 MatCap 默认
_UseMatCap=0,需要先设为 1.0 才能显示_MatCapTex
素材/资产研究技巧(MCP 远程查看素材用途)
当需要远程研究 Unity 项目中的素材(贴图、材质、prefab)但无法直接在本地打开时:
设为 1.0 才能显示 _MatCapTex
素材/资产研究技巧(MCP 远程查看素材用途)
当需要远程研究 Unity 项目中的素材(贴图、材质、prefab)但无法直接在本地打开时:### 关键限制
- MCP manage_asset 没有 list/search action — 只能通过 get_info 获取单个资产信息
- MCP 没有 Project 面板遍历工具 — 无法直接列出文件夹内容
- find_in_file 只支持 .cs 脚本 — 不能搜索素材路径
解决方法:C# execute_code + System.IO
使用 execute_code 通过 Roslyn 编译 C# 代码来遍历文件系统:
// 列出目录下所有文件(排除 .meta)
var appPath = UnityEngine.Application.dataPath;
var rootDir = System.IO.Path.GetDirectoryName(appPath);
var dir = System.IO.Path.Combine(appPath, "popopolibrary");
if (System.IO.Directory.Exists(dir)) {
var files = System.IO.Directory.GetFiles(dir, "*.*", System.IO.SearchOption.AllDirectories);
foreach (var f in files) {
if (!f.EndsWith(".meta")) {
var relPath = f.Substring(rootDir.Length + 1).Replace("\\\\", "/");
// 处理 relPath
}
}
}
读取材质信息
// 加载材质
var path = "Assets/xxx/SomeMaterial.mat";
var mat = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Material>(path);
// 获取 shader 名称
var shaderName = mat.shader.name;
// 获取所有纹理贴图
var texNames = mat.GetTexturePropertyNames();
foreach (var tn in texNames) {
var tex = mat.GetTexture(tn);
if (tex != null) {
var texPath = UnityEditor.AssetDatabase.GetAssetPath(tex);
// texPath 是项目相对路径
} else {
// 贴图通道为 null
}
}
// 读取 float/color 属性(如果有 using 限制,用完整类型名)
var floatVal = mat.GetFloat("_SomeFloat");
var colorVal = mat.GetColor("_SomeColor");
// 找到指定 GameObject 的渲染器
var go = UnityEngine.GameObject.Find("Body");
var mr = go.GetComponent<UnityEngine.SkinnedMeshRenderer>();
// 遍历材质槽
for (int i = 0; i < mr.sharedMaterials.Length; i++) {
var mat = mr.sharedMaterials[i];
var matPath = UnityEditor.AssetDatabase.GetAssetPath(mat);
// Slot i: matPath
}
// 或者遍历场景中所有渲染器
var allRenderers = UnityEngine.Object.FindObjectsOfType<UnityEngine.SkinnedMeshRenderer>();
常见素材研究场景
| 场景 | 方法 |
|---|---|
| 这个目录有哪些文件? | System.IO.Directory.GetFiles() |
| 这个材质用什么 Shader? | mat.shader.name |
| 这个材质用了哪些贴图? | mat.GetTexturePropertyNames() + mat.GetTexture() |
| 当前模型渲染器用了哪些材质? | 遍历 SkinnedMeshRenderer.sharedMaterials |
| 模型上有没有眼睛专用材质? | 检查材质路径是否包含 "eye"/"Eye"/"目" |
| 贴图的具体路径? | AssetDatabase.GetAssetPath(tex) |
注意事项
- 不要用
using语句 — execute_code 不支持,必须用完整类型名 - 返回字符串用
return,长度不限 System.Text.StringBuilder默认不可用,需用完整类型名或提前声明GetTexturePropertyNames()返回所有纹理属性名- lilToon 材质常用通道:
_MainTex(主贴图),_Main2ndTex(第二层),_Main3rdTex(第三层),_MatCapTex(MatCap),_EmissionMap(自发光) Hidden/lilToonOutline是 lilToon 的 Outline 变体 dTex(第三层),_MatCapTex(MatCap),_EmissionMap`(自发光)Hidden/lilToonOutline是 lilToon 的 Outline 变体## CRITICAL: write_file OVERWRITES — NEVER use it to append Thewrite_filetool always overwrites the entire file. If you need to append content to an existing file:- Read the file first with
read_file() - Combine old + new content in your code
- Write the combined content with
write_file()— but this creates a NEW file with BOTH old and new content OR: usepatch()for targeted edits between existing lines. OR: useterminal("cat >> file")for simple appends.
The write_file → overwrite mistake is extremely dangerous. 70KB+ files can be lost in an instant. Always read before write when appending. extremely dangerous. 70KB+ files can be lost in an instant. Always read before write when appending.### VRCExpressionsMenu 参数链路分析
当需要远程检查 VRCExpressionsMenu 的 Toggle 控件是否正确连接到 Animator Controller 时:
1. 找到 Menu Asset 不要用中文路径硬编码(会导致 execute_code 返回空字符串),用 FindAssets 按名字搜索:
var guids = UnityEditor.AssetDatabase.FindAssets("ClothType_未分类 t:VRCExpressionsMenu");
if(guids.Length==0) return "not found";
var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[0]);
var menu = UnityEditor.AssetDatabase.LoadAssetAtPath<VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionsMenu>(path);
2. 读取控件列表
// 基础信息 — 安全(无中文输出)
var sb = new System.Text.StringBuilder();
foreach(var c in menu.controls) {
sb.AppendLine("type="+c.type+" name="+c.name+" param="+(c.parameter?.name??"null"));
}
return sb.ToString();
3. 获取 Animator Controller
var desc = UnityEngine.Object.FindObjectOfType<VRC.SDK3.Avatars.Components.VRCAvatarDescriptor>();
var ac = desc.gameObject.GetComponent<UnityEngine.Animator>().runtimeAnimatorController
as UnityEditor.Animations.AnimatorController;
// ac.layers, ac.parameters 可用
4. 检查 AnyState 过渡条件(常用模式)
var layer = ac.layers[23]; // WardrobeCloth 层
var sm = layer.stateMachine;
foreach(var t in sm.anyStateTransitions) {
sb.AppendLine(t.destinationState.name);
foreach(var c in t.conditions)
sb.AppendLine(" "+c.parameter+" "+c.mode+" "+c.threshold);
}
for(int i=19; i<24; i++) {
var layer = ac.layers[i];
var sm = layer.stateMachine;
foreach(var s in sm.states) {
foreach(var t in s.state.transitions) {
if(t.conditions!=null)
foreach(var c in t.conditions) { /* c.parameter, c.mode, c.threshold */ }
}
}
}
常见链路模式:
VRCExpressionsMenu Control (Toggle type=102)
→ parameter.name (必须设置,否则是空壳按钮)
→ AnimatorController 参数 (Bool/Float/Int)
→ Layer AnyState 过渡条件 (WardrobeCloth 层,按值切换状态)
→ Layer 溶解层 (用条件 On/Off 切换溶解动画)
⚠️ Toggle vs Int 参数不匹配: Toggle 控件只能发送 0/1。如果它绑定的参数是 Int(期望多级值如 0~4),Toggle 实际只会设值为 1,达不到预期的多级效果。这种情况下需要把 Control type 改为 Radial Puppet 或使用 SubMenu + 多个 Button。 级值如 0~4),Toggle 实际只会设值为 1,达不到预期的多级效果。这种情况下需要把 Control type 改为 Radial Puppet 或使用 SubMenu + 多个 Button。### Console 日志分析方法
当检查过程中需要读取 Unity Console:
curl -s -X POST "$BASE/mcp" -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "MCP-Session-Id: $SID" \
-d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"read_console","arguments":{"count":50,"types":["error","warning","log"],"format":"plain"}},"id":101}'
常见值得关注的日志模式:
- "must be marked as Legacy" — Animation 组件使用的 Clip 缺少 Legacy 标记,需要勾选 AnimationClip.legacy = true
- "Loaded data for an avatar we do not own, clearing blueprint ID" — 加载了不属于你的 avatar,SDK 自动清空了 blueprint ID(非严重错误)
- 任何 NullReferenceException — 需要排查
- lilToon 相关的警告 — 材质配置问题
execute_code C# 操作指南
execute_code 使用 Roslyn C# 编译器,代码必须 return 一个 string。
⚠️ 关键认知:execute_code 能做的不只是"查"
这是我之前犯过的错误 — 以为 execute_code 只能查东西(读材质、看状态机),但实际上它可以执行任意 Unity Editor API 调用,包括修改 AnimatorController、创建 Layer、设置状态和过渡条件。它和写 Editor 脚本是完全一样的能力。
不要因为它叫"execute_code"就以为它功能受限 — Roslyn C# 编译器可以调所有 Unity Editor 命名空间,包括:
- UnityEditor.Animations.AnimatorController
- UnityEditor.Animations.AnimatorControllerLayer
- UnityEditor.Animations.AnimatorState
- UnityEditor.Animations.AnimatorStateTransition
- UnityEditor.Animations.BlendTree
- UnityEditor.Animations.ChildAnimatorState
遇到需要批量操作 Animator 的任务(创建 Layer、搭 4 状态循环、设过渡条件),直接写 C# 脚本用 execute_code 跑,不要想着"转成 MCP 命令"。 tate`
遇到需要批量操作 Animator 的任务(创建 Layer、搭 4 状态循环、设过渡条件),直接写 C# 脚本用 execute_code 跑,不要想着"转成 MCP 命令"。### 关键技巧
- C# 字符串转义:在 JSON 中 C# 字符串内的双引号必须转义为 \\\"
- 不要用 using 语句(不允许),直接用完整类型名
- UnityEditor.Animations 命名空间可能不完整 — AnimatorControllerParameterType 需要使用 UnityEngine.AnimatorControllerParameter + AddParameter() 方法
- 读取 VRCExpressionsMenu:用 Resources.FindObjectsOfTypeAll<VRCExpressionsMenu>().FirstOrDefault(x=>x.name==\"MenuName\")
- 读取 VRCExpressionParameters:同上模式
- 读取 AnimatorController:用 AssetDatabase.LoadAssetAtPath<AnimatorController>(\"path\")
- 修改 AnimationClip:new AnimationClip() → SetCurve(path, typeof(GameObject), \"m_IsActive\", curve) → AssetDatabase.CreateAsset(clip, path)
SetCurve(path, typeof(GameObject), \"m_IsActive\", curve)→AssetDatabase.CreateAsset(clip, path)`### AnimatorController 修改模板
// 加载 Controller
var path = "Assets/Cazalis/Animation/FX/Cazalis_FX_Modified.controller";
var ctrl = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEditor.Animations.AnimatorController>(path);
// 添加 Int 参数
ctrl.AddParameter("Clothes", UnityEngine.AnimatorControllerParameterType.Int);
// 创建新 Layer
var layer = new UnityEditor.Animations.AnimatorControllerLayer();
layer.name = "SetA_Transition";
layer.defaultWeight = 1f;
layer.blendingMode = UnityEditor.Animations.AnimatorLayerBlendingMode.Override;
// 创建 StateMachine
var sm = new UnityEditor.Animations.AnimatorStateMachine();
sm.name = "SetA_SM";
layer.stateMachine = sm;
ctrl.AddLayer(layer);
// 创建 4 个状态
var stateA = sm.AddState("A_Closed");
stateA.motion = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AnimationClip>("Assets/.../OFF.anim");
stateA.writeDefaultValues = false;
var stateB = sm.AddState("B_Appearing");
stateB.motion = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AnimationClip>("Assets/.../ON.anim");
stateB.writeDefaultValues = false;
var stateC = sm.AddState("C_Opened");
stateC.motion = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AnimationClip>("Assets/.../ON.anim");
stateC.writeDefaultValues = false;
var stateD = sm.AddState("D_Disappearing");
stateD.motion = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.AnimationClip>("Assets/.../OFF.anim");
stateD.writeDefaultValues = false;
oadAssetAtPath<UnityEngine.AnimationClip>("Assets/.../OFF.anim");
stateD.writeDefaultValues = false;// 设置过渡:A → B(Int == N)
var a2b = stateA.AddTransition(stateB);
a2b.hasExitTime = false;
a2b.hasFixedDuration = true;
a2b.duration = 1.0f; // 溶解时长
a2b.interruptionSource = UnityEditor.Animations.TransitionInterruptionSource.CurrentStateThenNextState; // ⚠️ 打断保护不可缺少
a2b.AddCondition(UnityEditor.Animations.AnimatorConditionMode.Equals, 1f, "Clothes");
// 设置过渡:B → C(播完自动进 C)
var b2c = stateB.AddTransition(stateC);
b2c.hasExitTime = true;
b2c.exitTime = 1f;
b2c.hasFixedDuration = true;
b2c.duration = 0f;
// 设置过渡:C → D(Int != N)
var c2d = stateC.AddTransition(stateD);
c2d.hasExitTime = false;
c2d.hasFixedDuration = true;
c2d.duration = 1.0f;
c2d.interruptionSource = UnityEditor.Animations.TransitionInterruptionSource.CurrentStateThenNextState; // ⚠️ 打断保护不可缺少
c2d.AddCondition(UnityEditor.Animations.AnimatorConditionMode.NotEqual, 1f, "Clothes");
// 设置过渡:D → A(播完自动回 A)
var d2a = stateD.AddTransition(stateA);
d2a.hasExitTime = true;
d2a.exitTime = 1f;
d2a.hasFixedDuration = true;
d2a.duration = 0f;
// 打断保护:B → A(Int != N)
var b2a_break = stateB.AddTransition(stateA);
b2a_break.hasExitTime = false;
b2a_break.hasFixedDuration = true;
b2a_break.duration = 0f;
b2a_break.AddCondition(UnityEditor.Animations.AnimatorConditionMode.NotEqual, 1f, "Clothes");
= 0f;
b2a_break.AddCondition(UnityEditor.Animations.AnimatorConditionMode.NotEqual, 1f, "Clothes");// 打断保护:D → C(Int == N)
var d2c_break = stateD.AddTransition(stateC);
d2c_break.hasExitTime = false;
d2c_break.hasFixedDuration = true;
d2c_break.duration = 0f;
d2c_break.AddCondition(UnityEditor.Animations.AnimatorConditionMode.Equals, 1f, "Clothes");
// 默认从 Entry 进入 A
var entry = sm.entryState;
var e2a = new UnityEditor.Animations.AnimatorTransition();
e2a.destinationState = stateA;
sm.AddEntryTransition(e2a);
// 保存
UnityEditor.EditorUtility.SetDirty(ctrl);
UnityEditor.AssetDatabase.SaveAssets();
UnityEditor.AssetDatabase.Refresh();
return "OK";
| 操作 | 代码 |
|---|---|
| 加载 AnimatorController | AssetDatabase.LoadAssetAtPath<AnimatorController>(path) |
| 添加参数 | ctrl.AddParameter("name", AnimatorControllerParameterType.Int) |
| 创建 Layer | ctrl.AddLayer(new AnimatorControllerLayer{name="X"}) 或用 new AnimatorControllerLayer() 直接设属性 |
| 添加状态 | sm.AddState("name") |
| 添加过渡 | stateA.AddTransition(stateB) |
| 设条件 | transition.AddCondition(AnimatorConditionMode.Equals, floatVal, "paramName") |
| 加载 AnimationClip | AssetDatabase.LoadAssetAtPath<AnimationClip>("path") |
| 保存 | EditorUtility.SetDirty(ctrl) + AssetDatabase.SaveAssets() + AssetDatabase.Refresh() |
| 加载 VRCExpressionParameters | AssetDatabase.LoadAssetAtPath<VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionParameters>(path) |
| 加载 VRCExpressionsMenu | AssetDatabase.LoadAssetAtPath<VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionsMenu>(path) |
| enu | AssetDatabase.LoadAssetAtPath<VRC.SDK3.Avatars.ScriptableObjects.VRCExpressionsMenu>(path) |
当 VRChat 模型项目中存在中文或日文资源名时,C# execute_code 返回的字符串在 JSON 序列化/反序列化过程中会损坏,导致返回空字符串或解析失败。
症状:C# 代码运行成功(status=success),但 data.result 为空字符串。
根因:中/日文字符在 C# 字符串字面量中写入时被 JSON 二次编解码,或者在拼接 StringBuilder 时遇到含非ASCII字符的字符串后整个输出被截断。
解决方案:
- 避免中文日文字符串字面量 — 不要写
"露肩短裙",用字节数组匹配:
// 匹配"露肩短裙"的UTF-8字节前缀 E9 9C B2
byte[] nb = System.Text.Encoding.UTF8.GetBytes(System.IO.Path.GetFileName(f));
bool isLujian = nb.Length >= 9 && nb[0]==0xE9 && nb[1]==0x9C && nb[2]==0xB2;
- 用索引替代中文名 — 在 AnimatorController 中通过参数索引/状态索引来引用,如
c.parameters[i].name不会输出到 JSON 中:
// 用参数索引代替参数名
var paramMap = new Dictionary<string, int>();
for (int i = 0; i < c.parameters.Length; i++) paramMap[c.parameters[i].name] = i;
// 输出时只输出索引值
sb.Append("p").Append(pidx);
- 用
System.IO.Directory.GetFiles绝对路径 — 通过文件系统遍历文件,再用AssetDatabase.LoadAssetAtPath加载,避免在 C# 代码中使用中文字符串硬编码路径:
string fxDir = System.IO.Path.Combine(UnityEngine.Application.dataPath, "Cazalis", "Animation", "FX");
var files = System.IO.Directory.GetFiles(fxDir, "*.anim");
// 用文件名精确匹配,而不是路径中的中文
var fileDict = new Dictionary<string, string>();
foreach (var f in files) {
if (!f.EndsWith(".meta")) fileDict[System.IO.Path.GetFileName(f)] = f;
}
- 输出时用短数字标识符 — 不用中文名/路径,用带分隔符的数字+简短ASCII字符串:
sb.Append("st").Append(i).Append("_crv=").Append(bindings.Length).Append("|");
// 而不是 sb.Append(l.name) 或 sb.Append(clip.name)
``### 常见错误
| 错误 | 原因 | 修复 |
|------|------|------|
|not all code paths return a value| 代码没 return | 加return "result";|
|AnimatorControllerParameterType找不到 | 命名空间引用问题 | 用UnityEngine.AnimatorControllerParameter+ctrl.AddParameter(param)|
|List没有Length| VRC 菜单 Control 是 List | 用.Count不是.Length|
|Keyframe(double,double)报错 | double→float 隐式转换 | 加 f 后缀:new Keyframe(0f, 1f)|
|NullReferenceException| Resources.Find 返回 null | 先检查 null,或改用 AssetDatabase.LoadAssetAtPath |
| **C# 返回空字符串**(status=success, result="") | 输出字符串含中文/日文字符,JSON 解析时损坏 | 用字节前缀匹配替代中文字符串字面量;用索引替代中文名输出;用 pipe(|)分隔、避免换行 |
| **路径StartsWith全部失败**(看似明明对得上) |Path.Combine(root, "A/B")**不规范化分隔符**,会保留/;而Directory.GetFiles()返回的子路径用`,混合后 StartsWith 字面不匹配 | 用多参数形式 Path.Combine(root, "A", "B"),再 .Replace('/', Path.DirectorySeparatorChar) 强制规范化 |
路径分隔符 bug(实战案例)
// ❌ 错(isoDir 含 / 残留)
var isoDir = System.IO.Path.Combine(root, "Assets/Cazalis_ZG_YH01_Assets");
foreach(var mf in System.IO.Directory.GetFiles(assetsRoot, "*.meta", ...)) {
if(mf.StartsWith(isoDir)) { ... } // 永远 False
}
// ✅ 对
var isoDir = System.IO.Path.Combine(root, "Assets", "Cazalis_ZG_YH01_Assets");
isoDir = isoDir.Replace('/', System.IO.Path.DirectorySeparatorChar);
症状:分类 dict 一个为 0、另一个全装;DRY-RUN 用 Directory.GetFiles(isoDir,...) 时正常(OS API 规范化了 isoDir),切到 GetFiles(assetsRoot,...) 全扫时崩盘。
DIAG 速查:分类失败时立刻打印 isoDir 字面值看是否混合 \ /。
规范化了 isoDir),切到 GetFiles(assetsRoot,...) 全扫时崩盘。
DIAG 速查:分类失败时立刻打印 isoDir 字面值看是否混合 \ /。### 模型结构须知(Cazalis 系列)
- Body ≠ Body_Base!Body 是面部(Cazalis_Face + Cazalis_Face_Alpha),Body_Base 是身体主体(Cazalis_Body)
- 两个模型都有的 22 个子节点结构完全一致(Armature, Body, Body_Base, Cloth_* 等)
- Body 有 2 个材质槽(Slot 0: Face, Slot 1: Face_Alpha),Body_Base 只有 1 个
- Cloth_Under_Bra 原版 active=false(默认隐藏),Cazalis zigai 版本需要手动设为 active=false 才能与原版行为一致
故障排除
Not Found(404) 在 GET 请求 → 应该用 POST resources/readSession not found→ session 已过期,重新 initializeNo instances→ Unity Editor 未启动 MCP SessionBad Request: Missing session ID→ 请求头没带 MCP-Session-IdNot Acceptable→ 缺少Accept: application/json, text/event-streamheaderNot Found在 /mcpforunity://* → 这是资源 URI,不是 HTTP 路径,要用 MCP 协议调用