内容
# omni-kit-mcp
驱动 **Omniverse Kit** 应用(如 Isaac Sim、Isaac Lab 或任何基于 Kit 的应用)的基础设施,通过 **Model Context Protocol** 进行交互。
**一个桥接器,一个端口,每个 Kit 进程。** 项目在命名空间下注册工具;项目之间不会发生命名冲突(两个项目可以分别提供 `play_scene`,它们分别作为 `arm.play_scene` 和 `rover.play_scene`),因此共同加载的项目共享同一个套接字。
```mermaid
flowchart TB
subgraph kit["Kit 进程(Isaac Sim · Isaac Lab · 任意 Kit 应用)"]
direction BT
BRIDGE["<b>omni.kit.mcp</b><br/>NDJSON TCP :9009 · 所有者/命名空间注册<br/>主线程调度<br/>内置功能:运行 Python · 列出工具 · 重载工具 · 列出桥接器"]
ARM["arm_tools/<br/>MCP_NAMESPACE = 'arm'"] -->|注册| BRIDGE
ROVER["rover_tools/<br/>MCP_NAMESPACE = 'rover'"] -->|注册| BRIDGE
PANEL["omni.kit.mcp.panel<br/>基于注册的 UI"] -->|"调度(点击 == 代理调用)"| BRIDGE
end
GW["kit-mcp 网关<br/>操作员视图:所有工具 + 运行 Python"] -->|MCP ⇄ NDJSON| BRIDGE
GWF["kit-mcp 网关(过滤)<br/>KIT_MCP_NAMESPACE=arm · KIT_MCP_BUILTINS=0"] -->|MCP ⇄ NDJSON| BRIDGE
CLI["kit_mcp.client<br/>脚本 · 测试 · 诊断 · 命令行界面"] -->|NDJSON| BRIDGE
```
## 安装(每台机器一次)
永久注册桥接器(之后,启动时无需标志或环境变量):
```bash
python3 scripts/install.py # 添加 --dry-run 来预览
```
这会编辑 Kit 应用的持久性 `user.config.json`(可脚本化的等效于扩展窗口的 AUTOLOAD 复选框):添加 `exts/` 作为搜索路径,自动启用 `omni.kit.mcp` + `omni.kit.mcp.panel`,并设置桥接器端口(默认为 **9009**)。重启应用;桥接器与内置工具绑定。`--remove` 可以撤销。
连接代理/IDE - 安装前端(`pip install -e .` 或 `uv sync`)并添加到 `.mcp.json`(Claude Code、Cursor 等):
```json
{
"mcpServers": {
"kit": { "command": "kit-mcp" }
}
}
```
不需要端口 - 网关发现一个单独运行的桥接器。固定一个端口,当多个 Kit 进程运行时,使用 `"env": { "OMNI_KIT_MCP_PORT": "9009" }`。
仅此一项即可完全使用:`run_python` 在 Kit 的主线程上执行任意 Python 代码(支持持久会话),可访问模拟中的所有内容。从桥接器发现的工具作为原生 MCP 工具出现(`robot_demo.spawn_robot` → `robot_demo__spawn_robot`;MCP 名称中不允许使用点号)。
## 添加项目(约定)
项目是一个 **纯 Python 包** - 不是 Kit 扩展。它声明一个命名空间并公开一个入口点(复制 `examples/demo_tools/`):
```python
# ~/myproj/isaac/myproj_tools/__init__.py
MCP_NAMESPACE = "robot_demo" # 默认值为模块名称
def register(registrar): # 接收一个所有者绑定的注册器
@registrar.tool("在场景中生成一个机器人。", {"name": {"type": "string"}})
def spawn_robot(name: str = "robot_1"):
import omni.usd # Kit 导入位于处理程序内部
...
return {"path": f"/World/{name}"} # 仅有效载荷 - 无信封
```
持久注册(每个项目一次):
```bash
python3 scripts/install.py add-project --path ~/myproj/isaac --module myproj_tools
```
下一次启动时,桥接器会广播 `robot_demo.spawn_robot`;面板会增加一个 `robot_demo` 部分;代理在 `refresh_tools` 后看到它。每次启动覆盖而不是持久化:`KIT_MCP_TOOL_PATHS=~/myproj/isaac KIT_MCP_TOOL_MODULES=myproj_tools`。
> **一次注册,三个界面。** 已注册的工具不仅是代理动词:控制面板会自动将其渲染为可点击的小部件,参考客户端/命令行界面可以从脚本调用它 - 所有这些都通过相同的调度入口。编写一次工具;按钮、代理调用和脚本调用随之而来。
处理程序约定:参数作为关键字参数到达;返回一个 JSON 可序列化的有效载荷(`None` → `{}`)或引发异常(`ToolError` 附加诊断信息,如捕获的 stdout)。同步或异步。桥接器拥有信封和主线程跳转。
**精选的每个项目代理视图** - 相同的二进制文件,相同的套接字,不同的环境:
```json
"robot-demo-agent": {
"command": "kit-mcp",
"env": { "OMNI_KIT_MCP_PORT": "9009",
"KIT_MCP_NAMESPACE": "robot_demo",
"KIT_MCP_BUILTINS": "0" }
}
```
仅暴露 `robot_demo.*` - 没有 `run_python` - 用于应保持在稳定 API 上的代理。`run_python` 是操作员的逃生阀,也是工具原型设计的方式;已注册的工具是代理依赖的内容。
## 脚本桥接器(无需 MCP)
桥接器的线路协议不是 MCP - 上面的网关是唯一的 MCP 说话者。对于脚本、测试和健康检查,使用 **参考客户端**(仅限标准库,单文件,也可以直接复制):
```python
from kit_mcp.client import call, BridgeClient, BridgeError
call("demo.ping", {"message": "hi"}) # 一次性;端口自动发现
with BridgeClient() as c: # 持久 + 重新连接
c.call("run_python", {"code": "result = 1"}) # (传递 port=... 来固定)
```
```bash
python3 -m kit_mcp.client <tool> ['{"json":"params"}'] [--port N]
python3 -m kit_mcp.client --list-bridges # 运行中的桥接器
# 退出代码:0 成功 · 1 桥接器回答错误 · 2 无法访问
```
**端口解析 - 您通常不需要指定一个。** 每个运行的桥接器都在每个用户运行时目录中广播其存在( `$XDG_RUNTIME_DIR/omni-kit-mcp/`)。客户端通过以下方式解析其端口:显式 `port=`/`--port` → `OMNI_KIT_MCP_PORT` → **当恰好一个桥接器运行时自动发现** → 当有多个桥接器运行时,报错并列出候选桥接器(例如,应用在 9009 *和* Lab 在 9010 上运行 - 然后命名一个)。单进程常见情况是零配置;只有真正模糊的多进程框需要端口。(通过网关的代理甚至更简单 - 网关在启动时解析一次,它们只是调用工具。)
`import kit_mcp.client` 无需安装 `mcp` SDK - 只有网关需要它。
## 热重载
编辑工具代码,然后(通过任意客户端):`reload_tools {"module": "myproj_tools"}` → 所有者释放未完成的任务,模块树重新导入(清除过时的字节码),工具重新注册。实时句柄必须存活(关节视图,物理句柄)属于名为 `*_state` 的子模块 - 状态模块在重载过程中保留。然后,MCP 前端的 `refresh_tools` 拾取架构更改。无需重启 Kit。
## 控制面板
`omni.kit.mcp.panel` 渲染实时注册:一个命名空间一个部分,每个工具的架构生成参数小部件。每个按钮都通过 `bridge.dispatch(...)` - 套接字请求采用相同的入口 - 因此人类点击和代理调用在构造上是相同的路径。想要定制 UI 的项目提供一个薄的仅小部件的扩展,遵循相同的规则。
## 线路协议(NDJSON over TCP)
每个 `\n`-终止的行是一个 JSON 对象,每个方向:
```
→ {"type": "robot_demo.spawn_robot", "params": {"name": "robot_1"}}
← {"status": "success", "result": {"path": "/World/robot_1"}}
← {"status": "error", "message": "...", "traceback": "...", ...}
→ {"type": "list_tools", "params": {}}
← {"status": "success", "result": {"tools": {"robot_demo.spawn_robot": {"description": ..., "parameters": ..., "namespace": "robot_demo", "owner_id": ...}}}}
```
格式错误的线路会收到一个错误响应;连接保持健康。许多客户端可以同时保持连接;每个连接都是隔离的。工具执行在 Kit 的主线程上序列化(USD/PhysX 不是线程安全的)。
## 配置参考
| 配置项 | 持久性(carb 设置在 `/persistent/exts/omni.kit.mcp/` 下) | 每次启动的环境变量覆盖 |
|---|---|---|
| 桥接器端口 | `autostartPort` | `OMNI_KIT_MCP_PORT` |
| 绑定地址 | `autostartHost` | `OMNI_KIT_MCP_BIND` |
| 工具包 | `toolModules` | `KIT_MCP_TOOL_MODULES` |
| 包路径 | `toolPaths` | `KIT_MCP_TOOL_PATHS` |
**远程访问。** 桥接器默认绑定 `localhost` - 它提供 `run_python`(远程代码执行按设计),因此监听环回之外是一个明确的每个盒子决策。选项:SSH 隧道(零配置),或将绑定地址设置为私有/尾部 IP(`install.py --bind-host <ip>`;从不 `0.0.0.0` 在公共主机上)。远程调用者无法读取盒子的端口文件,因此 `list_bridges` 内置函数存在:询问任何可达的桥接器(通常是稳定的已安装应用端口),它会报告其盒子上的每个桥接器 - 包括短暂端口的兄弟节点。
前端(`kit-mcp`)环境:`OMNI_KIT_MCP_PORT`(可选 - 当恰好一个桥接器运行时自动发现;设置它来固定一个进程),`OMNI_KIT_MCP_HOST`,`KIT_MCP_NAMESPACE`(逗号允许列表),`KIT_MCP_BUILTINS`(`0` 隐藏 `run_python`/`reload_tools`),`KIT_MCP_NAME` / `KIT_MCP_INSTRUCTIONS`.
## Isaac Lab(独立脚本)
桥接器的唯一 Kit 依赖项(`omni.kit.async_engine`)在 Kit 的核心扩展集发货,因此它在任何 Kit 应用中运行 - 包括 Isaac Lab。实验室**独立脚本**(训练/播放)与 Isaac Sim 应用有三个不同之处:脚本拥有 Kit 应用(`AppLauncher` + Lab 自己的 `isaaclab.python.*.kit` 体验),因此持久注册不适用 - 通过 `--kit_args` 启用桥接器每个脚本;脚本拥有主循环,因此桥接器命令仅在应用被泵送时执行(`env.step()` / `sim.step()`)- 阻塞的脚本在下一个泵送之前暂停调度,直到下一个泵送;并且配置仅限环境变量。请注意端口:“一个桥接器,一个端口,*每个 Kit 进程*” - 实验室独立运行是其自己的 Kit 进程,因此当它与已安装的应用(占用 9009)一起运行时,它需要自己的端口(按照惯例为 9010)。有关模板,请参阅 `examples/isaaclab_bridge_launch.py`。
## 布局
```
exts/omni.kit.mcp/ 桥接器扩展(协议,桥接器,内置功能,自动加载)
exts/omni.kit.mcp.panel/ 基于注册的控制面板(可选)
kit_mcp/ MCP 前端(FastMCP 服务器,桥接器的 TCP 客户端)
scripts/install.py 永久注册:基础安装 + 添加项目
examples/demo_tools/ 模板工具包 - 复制以启用项目
tests/ 协议 / 注册 / 自动加载 / 实时套接字测试(无需 Kit)
```
## 开发
```bash
python3 -m pytest tests/ # 套接字服务器在纯 asyncio 下运行在测试中
```