DCC 运行调试手册 (DCC-MCP-Debug)
这套方法是用户自己的工具/工作流,不是 Autodesk 官方文档记录的功能—— 不要把它当成"MotionBuilder/3ds Max 内置能力"介绍给别人,也不要在其它机器上假设它一定存在 (得先用"安装DCC连接器"流程装好)。
背景
DCC 软件(MotionBuilder、3ds Max、Maya)通常自带一个内嵌 Python 解释器,版本往往和系统里独立安装的
Python 不一致。如果某些依赖包(比如某个 .pyd 编译扩展)是专门编译给 DCC 内嵌 Python 版本的,
在系统独立 Python 下 import 会直接失败(ModuleNotFoundError 或 ABI 不匹配)。这种情况下,
唯一能"跑起来看效果"的办法就是让 DCC 自己执行代码——这份手册记录的就是怎么从外部(比如这个
agent 所在的 shell)把代码注入到正在运行的 DCC 进程里执行,而不用用户手动去点。
原理
DCC 软件装好 commandPort 连接器后,会监听一个本地 TCP 端口,端口号等于该 DCC 进程自己的
PID,收到的原始文本会被当成 Python 代码 exec() 执行。具体是哪个插件/怎么装的见下面
"安装 DCC 连接器"。
exec() 执行的是一段匿名字符串,不是真实文件,所以像 os.path.dirname(__file__) 这种写法本来
会报 NameError: name '__file__' is not defined——用脚本文件路径调用 exec 时默认会自动垫一行
__file__ = "<脚本绝对路径>" 绕开这个问题(可以用 --no-file-shim 关掉;-c 内联代码模式没有
文件路径,不垫这行)。
这个命令端口是个有状态的 REPL:一条连接建立时先吐一次 Python 版本 banner,之后同一条连接上
可以连续发多条命令、各自收响应,不需要每条命令都重新连接。dcc_service.py 就是利用这一点,给
每个注册的 DCC 各维护一条常驻连接,所有 exec 请求都在对应连接上排队执行
(dcc_bridge.DCCConnection 内部加了锁)。
安装 DCC 连接器(commandPort)
dcc_mcp_debug/plugin/ 下是 commandPort 插件本身(各 Python 版本的 .pyd + 3ds Max 的 .ms、
MotionBuilder 的 .py、Maya 的 .mod 模块声明 + 模块里的 userSetup.py)——这就是上面"原理"里说的那个"黑盒 TCP 执行入口"的真身。它随包分发,pip install
之后就在安装好的包里,不需要另外找文件。
优先用自动安装(不用查版本对照表、不用先知道装在哪个盘):
- MCP 工具
dcc_install_connector(先带dry_run: true预演一次) - 命令行
dcc-cli mobu install-connector/dcc-cli max install-connector/dcc-cli maya install-connector
它会调用目标 DCC 自带的独立解释器(mobupy.exe / 3dsmaxpy.exe / mayapy.exe)问出 Python 版本和 sys.path,
据此挑对应的 .pyd、并选对该装哪一层 site-packages(MotionBuilder 各版本有两层 site-packages 且都在
sys.path 上,靠"哪个目录存在"判断会装错),最后放好启动脚本。目标 DCC 没在运行时用 --install-dir /
install_dir 指定安装根目录。装进 Program Files 需要管理员权限,权限不足会明确报错;装完必须
重启该 DCC 才生效。
手工步骤和版本对照表见 https://github.com/pitelink/dcc-debug/blob/main/dcc_mcp_debug/plugin/安装说明.md,作为兜底
(自动安装失败、或目标版本不在支持范围时)。
用法(推荐):统一 client + 统一 server
客户端 dcc_mcp_debug/dcc_client.py 和服务端 dcc_mcp_debug/dcc_service.py 都是统一、不分 DCC 的单一进程:
只有一个常驻服务、一个控制端口(127.0.0.1:47863),内部按 dcc_bridge.DCC_REGISTRY
({"max": "3dsmax.exe", "mobu": "motionbuilder.exe"})给每个 DCC 各开一条连接。客户端每次请求
都带一个 dcc 标签(max/mobu),服务端照标签查到对应连接转发过去——两个 DCC 之间不做进程/
端口隔离,谁在跑就用谁的连接,互不影响。
统一客户端支持显式指定目标 DCC 或在单软件运行时自动嗅探目标 DCC。
# 1. 自动检测(当本地只有 3ds Max / MotionBuilder / Maya 之一运行时,无需输入目标名称)
dcc-cli status
dcc-cli exec -c "<Python代码字符串>"
# 2. 显式指定 3ds Max (max / 3dsmax)
dcc-cli max status
dcc-cli max exec -c "<Python代码字符串>"
dcc-cli max exec "<要跑的脚本路径.py>"
# 3. 显式指定 MotionBuilder (mobu / motionbuilder)
dcc-cli mobu select <PID> # 多个实例时记住"当前操作的"(状态存常驻服务,跨会话不丢)
dcc-cli mobu status
dcc-cli mobu exec -c "<Python代码字符串>"
dcc-cli mobu exec "<要跑的脚本路径.py>"
# 4. 显式指定 Maya (maya)
dcc-cli maya select <PID> # 多个实例时记住"当前操作的"
dcc-cli maya status
dcc-cli maya exec -c "<Python代码字符串>"
dcc-cli maya exec "<要跑的脚本路径.py>"
dcc-cli是 pip 装出来的命令行入口(= 本包dcc_mcp_debug/dcc_client.py的run_cli)。 没装包、只想用目录里的脚本时,等价写法是python -m dcc_mcp_debug.dcc_client ...。
执行模式支持:
exec -c "<code>":直接传入单行或多行 Python 代码字符串,不读写磁盘(推荐在交互式调试中优先使用)。exec <script.py>:执行本地现有文件。exec -:从标准输入 stdin 读取代码流。
exec 可以直接发送未经修改的脚本文件。exec 返回结果直接打印到 stdout,就是 DCC 命令端口的
原始响应文本(连接时的 Python banner 会被常驻服务在建连接那一步内部消费掉,不会混进 exec 的
输出里):
- 只有
>>>→ 脚本执行到底、没有抛异常。 - 带
Traceback (most recent call last):...→ 照常规 Python 报错读 traceback 定位问题。
调用优化指南(AI 助手重点遵循)
底层是直接的 socket 请求/响应,不涉及磁盘 I/O,单次调用很快;如果把一次代码验证拆解成"先写临时 文件 -> 再调 client 执行 -> 再删临时文件"三步,会白白多出两轮工具调用/决策延时,且没有必要。
- 即时性指令、测试代码或快速操作,优先用
exec -c "<代码>"单步直发,不要写临时文件。 - 只有在执行现有工程脚本、或大段已有文件内容时,才用
exec <文件路径>。 - 需要把少量数据从 DCC 内部传回来,走
raise ValueError(...)/ 普通异常通道(见下面"复用要点"里 关于SystemExit的禁令)。
status 打印一段 JSON,字段含义:running(对应 DCC 进程是否还在跑)、pid、connected(常驻
服务当前是否有一条打通的连接)、connected_pid、connected_at、last_error;dcc_client.py 会
再加一个顶层 dcc 字段标注这条状态是哪个标签的(不带标签、且当时有多个 DCC 在跑时,status 会
直接返回 {"max": {...}, "mobu": {...}} 这样按标签分组的整体状态)。
如果 client 本身报错说连不上/拉不起服务,可以直接看 %LOCALAPPDATA%\DCC-MCP-Debug\dcc_service.log 排查(只有这一份日志,
因为只有一个常驻服务进程)。
MCP 服务:DCC-MCP-Debug
把上面这套能力包成了一个 MCP 服务(dcc_mcp_debug/dcc_debug_mcp.py),名字叫 DCC-MCP-Debug,只依赖 Python 标准库,没有 mcp 包也能跑。
它不依赖当前目录,任何位置、任何支持 stdio MCP 的 AI 客户端都能用。
方式一:pip 安装(推荐,别人拿去用最省事)
目录里有 pyproject.toml,装完得到两个命令行入口:dcc-mcp-debug(MCP 服务)和 dcc-cli(即原来的 dcc_client.py)。
pip install git+<仓库地址>
改脚本时建议用可编辑安装,避免 site-packages 里出现一份和包目录不一致的副本:
python -m pip install -e "<本包目录>"
装好后,AI 客户端的配置只需要写命令名,不用关心脚本路径和 Python 路径:
{
"mcpServers": {
"DCC-MCP-Debug": { "command": "dcc-mcp-debug" }
}
}
GUI 客户端不一定继承 PATH;如果找不到 dcc-mcp-debug,把 command 换成 pip 那个 Python 的 Scripts 目录里的绝对路径,
例如 "<Python 安装目录>\\Scripts\\dcc-mcp-debug.exe",仍然不用写脚本路径。
Claude Code:
claude mcp add --scope user DCC-MCP-Debug -- dcc-mcp-debug
方式二:不安装,直接用目录里的脚本
command 写 Python 绝对路径(GUI 客户端不一定继承 PATH),args 用 -m 指向包模块;
把整个 DCC-MCP-Debug 目录放在哪里都行,只要那个目录的父目录能被 -m 找到:
{
"mcpServers": {
"DCC-MCP-Debug": {
"command": "<Python 安装目录>\\python.exe",
"args": ["-m", "dcc_mcp_debug.dcc_debug_mcp"],
"env": { "PYTHONPATH": "<本包目录>" }
}
}
}
不管哪种方式,只需给客户端这几个工具的描述就能用;dcc_exec / dcc_debug_read 的工具描述里已经内置了关键注意事项(禁用 SystemExit、3ds Max 要 redrawViews()、同步输出走返回值而异步回调报错走调试通道),
所以外部 AI 客户端不必依赖客户端的 CLAUDE.md 说明也能按规矩用它。
换一台机器时:整个 DCC-MCP-Debug 目录拷过去(dcc_mcp_debug/ 里的模块互相依赖、plugin/ 是连接器文件,都在包里),再把路径改掉。
工具一览(12 个):
- 执行:
dcc_exec(code或file)、dcc_reload_modules - 实例:
dcc_instances、dcc_select、dcc_status - 连接器:
dcc_install_connector - 调试:
dcc_debug_start_server、dcc_debug_connect、dcc_debug_read、dcc_debug_status、dcc_debug_clear、dcc_debug_disconnect
连接即启用调试:dcc_select 选定实例时,会自动在该实例里启动 debugpy(已有端口就复用)并接入;没调过 dcc_select、直接对某实例第一次 dcc_exec 时也一样。
自动接入失败不影响命令执行,只在返回里多一句说明,60 秒内不重试。dcc_select(debug=false) 可以只连命令通道(比如要把调试端口留给 VSCode)。
手动 dcc_debug_disconnect 过的实例不会再自动重连,要再接入就重新 dcc_select 或 dcc_debug_connect。多个 DCC 同时在跑时,不带 dcc 参数的调用沿用最近 dcc_select 选过的那个。
dcc_debug_disconnect 里必须先 shutdown 再 close 套接字:读取线程持有 makefile(),只 close() 的话底层连接不会真关,服务端会一直认为客户端还在,
重连报 Server[pid=N] is already being debugged,VSCode 也接不上。
多实例:全部保持连接,只记住"当前操作哪个"
同一个 DCC 可能同时开着多个版本/窗口(例如 MotionBuilder 2020 和 2024,PID 不同)。常驻服务给每个实例各保持一条命令连接
(InstancePool,按 PID 懒创建,进程退出时清理),并用一个变量记住当前操作的是哪个实例。
状态放在常驻服务里,但生命周期跟着客户端走:不放 MCP 进程(MCP 进程会随会话 resume/重连被重启,放在它内存里等于随机丢);
放常驻服务是为了让它跨 MCP 进程重启存活,但客户端一退出就释放——常驻服务会记下每样东西是哪个客户端进程占的,
客户端优雅退出时主动告知(MCP 的 atexit),被强杀时由服务端每 5 秒扫一次进程存活兜底。
新会话不会继承上一个会话的选择和调试连接,需要重新说"连接DCC"。
- 切换实例(
dcc_select(pid);命令行dcc-cli mobu select <pid>,不带 pid = 取消选择)只改这个变量,不断开任何连接; 连接状态可疑时用dcc_select(pid, reconnect=True)(命令行dcc-cli mobu select <pid> --reconnect)强制把该实例的命令连接丢掉重建—— 只影响这一个实例,别的不动;每个实例在 DCC 里的全局变量(dcc_exec的跨调用状态)也各自保留。 - 用户要求"连接 DCC"、或要操作哪个不明确时:先
dcc_instances看实例。只有一个就直接用;有多个用AskUserQuestion问用户, 选项写清 版本 + PID + 启动时间(如"MotionBuilder 2024 · PID · 启动时间"),再dcc_select。不要自己按版本号、启动先后去猜。 - 没选择又有多个实例时,
dcc_exec会直接报错并列出 PID(故意的,不要绕过)。CURRENT指向的实例退出后也会报错并清除选择,不会悄悄换到另一个。 - 任何调用都可以带
pid参数临时打到某个实例,不改变CURRENT。命令行同理:dcc_client.py mobu instances、dcc_client.py mobu exec --pid <PID> -c "..."。 - 调试端口按实例各自保存,不再固定 4345:
dcc_debug_start_server让目标实例在随机空闲端口启动 debugpy(该实例已有就复用),dcc_debug_connect自动从该实例的 debugpy 适配器子进程命令行(--port N)找端口,所以连接器插件/VSCode 扩展启动的端口也能找到。 每个实例各有独立的调试连接和事件缓冲区,可以同时连着多个,切换当前实例时互不影响。 调试会话同样住在常驻服务里(dcc_service.sessions,实现见dcc_dap.py),不放 MCP 进程,理由同上; 客户端退出时连会话一起断开,调试端口随之释放(否则会被一直占着,VSCode 之类接不上)。 MCP 只负责转发,并把常驻服务里新到的事件轮询转成客户端通知(notifications/message)。 dcc_debug_connect的 attach 请求必须带"redirectOutput": True,否则 debugpy 不会接管sys.stdout,print到不了调试通道(MotionBuilder 的sys.stdout默认是它自己的_ToListener)。经dcc_exec执行的代码 stdout 被包装器重定向回结果里,也不会进调试通道;调试通道抓的是工具自己运行时 (比如你在界面点"执行")的输出,以及dcc_client.py exec直发代码的输出。dcc_exec里抛的异常走命令响应,不在调试通道里。dcc_exec的包装器(dcc_exec_tools.py的EXEC_WRAPPER)Python 2.7(MotionBuilder 2020)和 3.x 通用,实测两个版本 13 个用例结果一致。设计依据:- 全局变量存在 DCC 内的
builtins.__mcp_globals__(2.7 里是__builtin__)。commandPort 每条连接的 globals 是各自独立的,超时后连接被我们重建, 变量存在 commandPort 自己的 globals 里就丢了,存在 builtins 里才能保留。 - stdout 用自己的
_Sink收集,不用io.StringIO(py2 里它只收 unicode,print写str会报错,pitelink.dcc-utils 扩展就是为此按版本分支导入)。 - traceback 去掉包装器自己和
ast.py的栈帧、补上源码行,py3 下带异常链(对应扩展的format_exception);py2 下带 coding 声明的代码会先把声明行换成#。 file参数执行磁盘上的 .py(utf-8),会设置__file__,traceback 显示真实文件名和行号;name参数设置__name__,不传则不设置 (此时__name__是builtins/__builtin__,setup.py的if __name__ in ("__main__","builtins")就是依赖这点)。- 命令长度没有限制(扩展里那个 1025 字节的检查是它自己加的,commandPort 本身 5000 字节也正常)。
- 全局变量存在 DCC 内的
- 进程扫描(
tasklist/ PowerShell CIM)每次约 300ms,而真正的命令往返只要约 30ms,所以 MCP 里把扫描结果缓存 3 秒;dcc_instances/dcc_select/dcc_status/dcc_debug_status强制刷新,服务报"实例已退出"时清缓存。dcc_exec因此从约 570ms 降到约 27ms。
架构
-
dcc_mcp_debug/dcc_bridge.py:连接管理核心。DCC_REGISTRY是"标签 → 进程名"的字典({"max": "3dsmax.exe", "mobu": "motionbuilder.exe"}),DCCConnection(process_name=...)是通用的 "按进程名找 PID、连 PID 端口、发送/接收、断线重连、查状态"逻辑,不知道也不关心自己具体是哪个 DCC, 不含任何网络服务/CLI 代码。所有subprocess.run(["tasklist", ...])调用都带了creationflags=CREATE_NO_WINDOW,否则dcc_service.py这种没有控制台的 detached 进程每次查 PID 都会在屏幕上闪一下黑色控制台窗口。连接活性检测分两层,成本从低到高:
DCCConnection._ping()(已有连接时的默认路径):在现有 socket 上发一个字节的"\n"(等价于exec("\n"),空语句零副作用),用专门的短超时PING_TIMEOUT = 2秒等回包。 不起子进程,纯本地回环收发,通常几毫秒内出结果;ping 通过就直接复用连接执行真正的代码 (用完整的self.timeout,不会因为脚本本身跑得久而被误判断线)。ping 失败(超时/报错) 才判定连接已死,转去重连。这层测的是"这条连接现在到底还能不能正常收发",比查进程列表更 直接、也更快发现"进程没死但主线程卡死"这种tasklist测不出来的情况。find_pid()/pid_alive()(只在没有连接、或 ping 判定连接已死需要重连时才用):pid_alive(pid, name)用tasklist /FI "PID eq <pid>" /FI "IMAGENAME eq <name>"只查一个 已知 PID,比find_pid()全量拉tasklist再逐行找快;DCCConnection._current_pid()封装了 "有已保存 PID 就先走pid_alive快路径,查不到/没有已保存 PID 才 fallback 到find_pid()全量扫"这套逻辑。这两个函数都要起tasklist.exe子进程,开销比_ping()大一个数量级以上 (进程创建本身的成本),所以只在必须重新定位 PID 时才用。
-
dcc_mcp_debug/dcc_service.py:单个常驻 daemon,按DCC_REGISTRY给每个标签各开一个DCCConnection(存在一个{tag: DCCConnection}字典里),对外只监听一个本地固定端口127.0.0.1:47863。收一行 JSON 请求({"cmd":"status","dcc":"max"}/{"cmd":"exec","dcc":"mobu", "script":...},status的dcc可省略表示查全部),按请求里的dcc标签查字典转发给对应连接, 返回一行 JSON 响应。日志在%LOCALAPPDATA%\DCC-MCP-Debug\dcc_service.log。两个 DCC 共用一个进程、一个端口,彼此之间 没有隔离,仅靠请求里的标签区分。 -
dcc_mcp_debug/dcc_client.py:统一 CLI 入口(命令行入口名dcc-cli),找不到常驻服务就自动用subprocess.Popen(detached, 不随本次命令退出被杀)拉起dcc_service.py,再带着dcc标签发请求。命令行第一个参数如果是max/mobu(或别名3dsmax/motionbuilder)就作为显式标签;省略时会看tasklist自动嗅探 (只有一个在跑就用那个,多个在跑则status汇总展示、exec报错要求显式指定,一个都没跑则直接 报"未运行"、不会去拉起常驻服务)。
要接入新的 DCC(只要它也走"TCP 端口 = 进程 PID,收到文本 exec()"这种 commandPort 机制),只需要在
dcc_bridge.DCC_REGISTRY 里加一行标签映射、在 dcc_client.DCC_ALIASES 里加对应别名,dcc_service.py
会自动为新标签开一个连接,不用碰连接管理逻辑本身。
评估过要不要直接做成 MCP server(Claude Code 原生管理生命周期、结构化工具调用),但那需要装
mcp 依赖、改 Claude Code 配置、且新注册的 MCP server 要重启会话才生效,所以先做成上面这套本地
daemon + client 的方案。核心逻辑已经拆进独立的 dcc_bridge.py,以后真要包一层 MCP server,只是
外面再套一层,不用重写连接管理。
复用要点
- DCC 重启后 PID 会变,旧连接的
_ping()会失败(进程没了,socket 直接断),dcc_service.py会在下一次exec/status请求时自动走重连路径、用find_pid()找到新 PID,不需要手动重启 常驻服务。 - 常驻服务是单实例的:
dcc_service.py启动时如果发现自己的控制端口47863已被占用(说明已有 一个在跑),会直接退出,不会有多个实例互相打架。 - 这个方法只能验证"代码跑起来会不会报错、日志/print 输出是什么",UI 视觉效果(弹窗 是否正常显示、控件排布好不好看)还是要用户自己盯着 DCC 窗口确认。
- 发进去的代码里绝对不要用
raise SystemExit(...)/sys.exit(...)之类退出调用来"把结果带出来" (比如想偷懒把要看的值塞进异常信息里传回来)。会把 commandPort 的监听/处理线程直接弄死:DCC 本体 进程不会崩、还在正常跑,但那之后这个 TCP 端口就再也连不上了(ConnectionRefusedError),必须 重启 DCC、让启动脚本重新import commandPort才能恢复。想要回传结果,用raise ValueError(str(...))(或其它普通 Exception)走 traceback 通道带回来,不要用 SystemExit。 - 如果 Bash 命令偶发报一个跟具体命令内容无关、看起来是环境本身的
add_item ... fatal error, 跟这套脚本无关,重跑一次通常就好了。 - 3ds Max 专属:改动场景(建物体、改属性、改材质等)的
max exec命令结尾要加一条刷新视口的 命令(pymxs.runtime.redrawViews()),否则改动要等下一次视口重绘才会显示出来,容易让人误以为 命令没生效。 - 连接失败自愈:进程在运行但连不上,大概率是这个实例没装 commandPort——静默按
https://github.com/pitelink/dcc-debug/blob/main/dcc_mcp_debug/plugin/安装说明.md补装(不问、不额外通知),装完不用在同一个 任务里重试连接,直接告诉用户"连接失败,请重启 <DCC 名称>"。
Metadata
Release files for dcc-mcp-debug 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| dcc_mcp_debug-0.1.0.tar.gz | 197.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dcc_mcp_debug-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 391.9 kB
Release files / dcc_mcp_debug-0.1.0.tar.gz
| Download URL | dcc_mcp_debug-0.1.0.tar.gz |
|---|---|
| Size | 197.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
60e749614ef03d79bf8b3847bdbbe7a11709dbb25584a8bf151e6b10060a9705
|
|
BLAKE2b-256 checksum How to use checksums |
7e11be1753fea1ae4aa62fd20df9f1265bdfc628d484a0b0dea4cb5d9b65cd15
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.11
|
Release files / dcc_mcp_debug-0.1.0-py3-none-any.whl
| Download URL | dcc_mcp_debug-0.1.0-py3-none-any.whl |
|---|---|
| Size | 194.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
de41995b9e58fc0b7cb6f9e5027a748afe064c337a0f70d99a342ef4b6a86fcb
|
|
BLAKE2b-256 checksum How to use checksums |
4921b48125aa66e4a34c70ed51285e437f5febd965d27c4b2e0026dd8c9d83ca
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.11
|