一、问题现象
现象其实相当直观:当你通过 BlenderMCP 让 AI(例如 Claude 或 Cursor)操控 Blender 时,MCP 服务会突然中断。插件面板中那个“Server Running”的勾选状态,也会莫名其妙地被取消。

先澄清一点:出问题的并不是 Blender 本身,它的进程始终稳定运行。真正挂掉的是 addon.py 中的 socket server 线程。每次服务一断,你就得手动重新勾选插件、重连 MCP 客户端,才能继续工作。对于 DaisySim 航空航天仿真项目来说,这种开发节奏的中断,代价可不小。
二、排查过程
2.1 第一反应:查找日志
既然是“MCP 服务崩溃”,第一反应自然是寻找崩溃日志。能查的地方基本都翻了一遍:
| 检查项 | 结果 |
|---|---|
| Windows 事件查看器 Application 日志(近 7 天 1000 条) | 0 条 Blender/Python/MCP 相关 |
| Windows 事件查看器 System 日志(近 7 天 1000 条) | 0 条相关 |
| WER ReportArchive / ReportQueue(Windows 错误报告) | 空 |
| CrashDumps 目录 | 0 个 dump 文件 |
%TEMP%blender_* 临时目录 | 5 个空目录,无日志 |
| Blender 5.1 配置目录 | 只有 userpref.blend、bookmarks.txt,无 .log 文件 |
| Blender 进程状态 | PID 7996,482MB,Running,正常 |
| MCP server 端口 9876 | 在线,get_scene_info 测试成功返回 |
最终只证实了一个事实:文件系统里根本不存在所谓的 MCP 服务崩溃日志。
2.2 为何没有日志
打开 addon.py 源码一看,所有错误处理都是这个套路:
except Exception as e:print(f"Error in server loop: {str(e)}")traceback.print_exc()
问题在于,print() 和 traceback.print_exc() 的输出目标是 stdout。而在 Windows 上通过 GUI 启动 Blender 时,stdout 只会输出到 System Console 窗口(菜单 Window > Toggle System Console),不会写入任何文件。这个控制台窗口一旦关闭,内容就彻底消失了。
因此,历史崩溃的 traceback 早就随着控制台输出一起烟消云散——这也就是“查不到日志”的根本原因所在。
2.3 第一轮代码分析:发现 BaseException 漏捕获
既然没有历史日志,那就只能从代码层面寻找源头。逐行分析 addon.py 后,揪出了几个可疑点:
except Exception 漏掉了 BaseException 子类。Python 异常体系是这样分级的:
BaseException├── SystemExit← 解释器退出时抛出├── KeyboardInterrupt ← Ctrl+C 时抛出├── Exception ← 所有“正常”异常的基类└── GeneratorExit
addon.py 的 _server_loop()、_handle_client()、execute_wrapper()、execute_command() 全部使用 except Exception,这意味着它们无法捕获 SystemExit 和 KeyboardInterrupt。当这些异常在线程中抛出时,线程会静默死亡——没有任何错误信息打印,self.running 仍然是 True,但线程实际上已经不在了。
那 checkbox 为何会被取消?scene.blendermcp_server_running(驱动 UI checkbox 的 BoolProperty)只在 StartServer/StopServer operator 执行时更新。线程静默死亡时,不会触发任何状态更新。真正的原因在于 Blender 的脚本重载机制:
unregister() 函数会执行:
del bpy.types.Scene.blendermcp_server_runningdel bpy.types.Scene.blendermcp_auto_start_server# ... 删除所有 Scene 属性
当 register() 再次执行时,这些属性被重新创建,默认值是 False:
bpy.types.Scene.blendermcp_server_running = bpy.props.BoolProperty(name="Server Running", default=False)
所以,checkbox 被取消=unregister/register 循环被触发。但到底是什么触发了这个循环?此时还是个谜。
2.4 给 addon.py 打补丁:捕获真实崩溃信息
既然原版代码不写日志,那就给它加上。我给 addon.py 打了补丁,做了 5 项改动:
- 添加模块级 logger:所有日志写入
blendermcp_debug.log文件 - 捕获 BaseException:所有线程循环的
except Exception改为except BaseException - 添加 watchdog timer:每 2 秒检查线程是否存活,死了就把 checkbox 自动设为 False 并记录原因
- 裸
except:改为except BaseException as e:并记录 - 新增
_crash_reason字段,保存崩溃原因
同时备份了原版到 addon.py.bak。
2.5 首次抓到崩溃日志
用户重载插件后正常使用,10:10:12 首次捕获到崩溃日志:
10:10:12,741 [BMCP-ClientHandler] Received command: execute_code10:10:12,760 [MainThread]stop() called, running=True10:10:12,760 [BMCP-ServerLoop] Error accepting connection: [WinError 10038]10:10:13,267 [MainThread]BlenderMCP addon unregistered
关键线索出现了:stop() 是在 [MainThread] 被调用的,而 execute_code 正是在主线程通过 bpy.app.timers 执行。中间只隔了 19ms。
这意味着,并非线程静默死亡,而是 execute_code 执行的代码主动调用了 stop()!
但此时还看不到 execute_code 到底执行了什么代码。于是,我又加了两处记录:
_handle_client收到execute_code时记录代码内容(前 300 字符)stop()里加traceback.format_stack()记录调用栈
2.6 破案:execute_code payload + stop() caller stack
用户再次重载插件后重现,10:13:31 的日志直接给出了铁证:
execute_code 执行的代码:
# Reset and re-import Hubble Space Telescopebpy.ops.wm.read_factory_settings(use_empty=True) ← 罪魁祸首bpy.ops.import_scene.gltf(filepath=r'D:worklogicspaceDaisySimpublicmodelsHubble Space Telescope (A).glb')...
stop() 的精确调用栈:
execute_wrapper→ execute_command→ _execute_command_internal→ execute_code→ exec(code, namespace)→ 用户代码第 2 行: bpy.ops.wm.read_factory_settings(use_empty=True)→ Blender 内部: addon_utils.disable_all()← 重置时卸载所有插件→ BlenderMCP.unregister()→ bpy.types.blendermcp_server.stop()← MCP 服务被停掉
三、根因分析
3.1 完整因果链
MCP 客户端发 execute_code(含 read_factory_settings(use_empty=True))→ Blender 执行“恢复出厂设置(空场景)”→ 内部调 addon_utils.disable_all() 卸载所有非默认插件→ BlenderMCP.unregister() 被触发→ bpy.types.blendermcp_server.stop()← socket 关闭,MCP 服务挂掉→ server_loop accept() 撞 None socket 报 WinError 10038→ Scene 属性全被 del(checkbox 重置为 False)
3.2 为什么 AI 会生成这种代码
AI 客户端(Claude/Cursor)在操控 Blender 时,经常遇到“清空当前场景再导入新模型”的需求。最直观的清空方式就是 bpy.ops.wm.read_factory_settings(use_empty=True)——这在人工操作时完全没问题,但在 MCP 场景下却是致命的,因为:
read_factory_settings会重置整个 Blender 到出厂状态- Blender 内部会调用
addon_utils.disable_all()卸载所有非默认插件 - BlenderMCP 作为第三方插件首当其冲被卸载
- 插件的
unregister()会调用server.stop()关闭 socket - MCP 连接断开,AI 客户端失去对 Blender 的控制
3.3 这不是 addon.py 的 bug
需要强调一点:这不是 BlenderMCP 插件的 bug,而是使用方式的问题。execute_code 接受任意 Python 代码并通过 exec() 执行,这在设计上就是“把 Blender 完全交给 AI 控制”。AI 生成的代码如果调用了会卸载插件本身的操作,自然会导致服务中断。
addon.py 原版的错误处理确实有改进空间(except Exception 漏捕获 BaseException、裸 except: 吞错误、不写文件日志),但这些都是次要问题——真正导致服务中断的,是 read_factory_settings 这个操作本身。
3.4 WinError 10038 是良性竞态
日志里反复出现的 OSError: [WinError 10038] 在一个非套接字上尝试了一个操作 是个良性竞态:
stop()把self.socket = None- server_loop 下一轮
accept()撞上 None socket 报错 - 这个错误被
except Exception捕获,不影响功能
看起来吓人,但只是 stop 过程中的副作用,并非崩溃原因。
四、修复方案
4.1 方案选择
| 方案 | 描述 | 优缺点 |
|---|---|---|
| A. 改 AI 客户端行为 | 在 system prompt 里告诉 AI 禁用 read_factory_settings | 简单,但依赖 AI 遵守,不可靠 |
| B. 拦截危险操作 | 在 execute_code 里检测危险操作并拒绝执行 | 可靠,AI 收到错误后会自动改用安全方式 |
| C. 保存连接重启 | 危险操作前保存客户端连接,操作后重启 server | 复杂,hacky |
最终选择了方案 B:在 execute_code 开头加危险操作拦截。这样一来,即使 AI 忘了规则,插件也会主动拦截,并返回错误信息(包含安全替代代码)。AI 收到错误后,就会自动改用安全方式。
4.2 实施的修复
在 execute_code 方法开头加入危险操作检测:
# 危险操作黑名单_DANGEROUS_OPS = ("bpy.ops.wm.read_factory_settings","bpy.ops.wm.read_homefile","addon_utils.disable_all","bpy.types.blendermcp_server.stop","bpy.ops.blendermcp.stop_server",)def execute_code(self, code):try:# 拦截会杀死 MCP 服务本身的操作for bad in self._DANGEROUS_OPS:if bad in code:msg = ("Blocked dangerous operation '%s' - it would unload ""the BlenderMCP addon and kill the MCP connection. ""Use this safe alternative to clear the scene:n""import bpyn""for obj in list(bpy.data.objects):n""bpy.data.objects.remove(obj, do_unlink=True)n""for coll in [bpy.data.meshes, bpy.data.materials,n""bpy.data.images, bpy.data.lights,n""bpy.data.cameras, bpy.data.actions]:n""for item in list(coll):n""if item.users == 0:n""coll.remove(item)") % bad_bmcp_logger.warning("execute_code BLOCKED: %s", bad)return {"executed": False, "error": msg, "blocked": True}# 正常执行namespace = {"bpy": bpy}capture_buffer = io.StringIO()with redirect_stdout(capture_buffer):exec(code, namespace)return {"executed": True, "result": capture_buffer.getvalue()}except Exception as e:raise Exception(f"Code execution error: {str(e)}")
4.3 安全的清空场景代码
替代 read_factory_settings(use_empty=True) 的安全方式:
import bpy# 删除所有对象(不会触发插件重载)for obj in list(bpy.data.objects):bpy.data.objects.remove(obj, do_unlink=True)# 清理孤立数据(mesh、material、image 等无引用的)for coll in [bpy.data.meshes, bpy.data.materials, bpy.data.images, bpy.data.lights, bpy.data.cameras, bpy.data.actions]:for item in list(coll):if item.users == 0:coll.remove(item)
效果一样(场景清空),但不会卸载插件,MCP 连接不受影响。
4.4 保留的诊断补丁
除了危险操作拦截,之前打的第一轮补丁也保留着:
- 文件日志:所有日志写入
blendermcp_debug.log,以后再有崩溃能直接看文件 - BaseException 捕获:防止线程静默死亡
- watchdog timer:线程死了自动同步 checkbox 状态
- execute_code 内容记录:记录执行的代码(前 300 字符)
- stop() 调用栈记录:记录 stop 是从哪调的
这些作为保险层,万一以后有其他类型的崩溃,能快速定位。
五、经验总结
5.1 “查不到日志”本身就是一个发现
排查初期花了很多时间找日志,最后发现文件系统里根本没有日志文件。这本身就是一个重要发现:addon.py 用 print() 和 traceback.print_exc() 输出错误,而 Windows GUI 启动的 Blender 不把 stdout 写入文件。
教训:如果一个程序依赖 stdout 输出错误信息,在 Windows GUI 环境下这些信息会随控制台窗口关闭而丢失。需要排查这类问题时,第一步应该给程序加文件日志,而不是花时间找不存在的日志文件。
5.2 “静默死亡”是第一假设,但要准备被推翻
代码分析发现 except Exception 漏捕获 BaseException,第一反应是“线程静默死亡导致 checkbox 不同步”。这个假设看起来很合理,但日志抓到后才发现真正的根因是 execute_code 主动调用 stop()。
教训:代码分析能发现潜在风险,但真实根因必须靠日志验证。不要在没有日志证据的情况下下结论。
5.3 execute_code 是双刃剑
BlenderMCP 的 execute_code 命令通过 exec() 执行任意 Python 代码,namespace 里注入了 bpy。这意味着 AI 可以做任何事情——包括杀死 MCP 服务本身。
教训:接受任意代码执行的接口必须有危险操作防护。不能假设调用方永远不做危险操作。
5.4 AI 生成代码需要场景感知
AI 不知道 read_factory_settings 在 MCP 场景下是危险的——它在训练数据里见过这个 API,知道它能清空场景,就用了。这种“语义正确但上下文危险”的代码是 AI 编程的典型陷阱。
教训:给 AI 用的工具接口,应该在错误信息里明确告诉 AI 正确的做法。这样 AI 收到错误后会自动调整,而不是反复尝试同样的错误方式。
5.5 Windows 排查的环境约束
排查过程中遇到多个环境约束:
- Bash 工具拦截带
#注释的命令 wmic/reg被安全策略禁用- PowerShell 工具异常
psutil不可用
最终用 Python 调 wevtutil(Windows 事件命令行工具)+ gbk 解码绕过了这些限制。
教训:Windows 环境下排查问题,Python 是最可靠的通用工具。subprocess.run(['wevtutil', ...]) 配合 decode('gbk', 'replace') 可以稳定查询 Windows 事件日志。
六、文件清单
| 文件 | 说明 |
|---|---|
addon.py | 已打补丁的插件主文件(危险操作拦截 + 日志 + watchdog) |
addon.py.bak | 原版备份 |
blendermcp_debug.log | 日志文件(插件重载后自动生成) |
patch_addon.py | 第一轮补丁脚本(日志 + BaseException + watchdog) |
BlenderMCP_诊断报告.md | 第一轮诊断报告 |
BlenderMCP_崩溃诊断与修复实录.md | 本文(完整排查过程 + 根因 + 修复) |
路径前缀:
- 插件文件:
C:UsersAdministratorAppDataRoamingBlender FoundationBlender5.1scriptsaddons - 工作目录:
C:UsersAdministratorWorkBuddy2026-07-02-09-03-18
本文记录了一次真实的问题排查过程,从“查不到日志”到“抓到铁证”再到“确认根因”和“实施修复”,完整呈现了 BlenderMCP 服务崩溃问题的诊断与解决。希望对遇到类似问题的开发者有所帮助。
