主题
Runtime MCP 调试指南
Runtime MCP 是编辑器调试模式下提供给 AI 助手的客户端运行时调试入口。它允许 AI 助手在本机调试客户端中调用一组隐藏的脚本工具,用来读取 UI 控件树、查询控件坐标、保存截图,或调用项目自己注册的调试工具。
这个能力只面向调试期,不是正式游戏功能。当前实现分两层:客户端进程监听本机 Runtime TCP bridge,编辑器 MCP 暴露固定官方工具 runtime_call_tool 并转发到这个 bridge。脚本侧工具不会出现在编辑器 MCP 的 tools/list 中。
TriggerMcpHost 与 Runtime MCP 无关。无法通过启动 TriggerMcpHost 获得客户端运行时调试能力。
适用场景
- UI 布局调试:让 AI 助手读取控件层级、文本、可见性和像素坐标。
- 视觉回归检查:保存当前客户端截图,并和 UI 结构化数据一起分析。
- 物理角色诊断:订阅服务端 MotorProbe,并读取连续物理帧的着地、速度与碰撞命中。
- 纯客户端调试:在不启动服务端的情况下验证 UI、Canvas、模型或本地工具逻辑。
- 项目自定义诊断:注册只在调试期可用的项目工具,例如导出当前 view model、读取本地预览状态、返回关卡辅助数据。
Runtime MCP 只连接本机客户端运行时,不连接服务端,也不读取远端 host 内部状态。
使用条件
Runtime MCP 需要通过编辑器调试启动客户端。支持普通调试、调试不编译,以及纯客户端调试。
运行时满足调试条件时,客户端侧 Game.IsDebugTestMode 为 true。纯客户端调试时,Game.IsClientOnly 也为 true。
不要在正式包、线上环境或普通非调试客户端中依赖 Runtime MCP。非调试启动不会开放这个入口。
多开与连接选择
GUI 编辑器及其调试 Runtime 默认自动分配可用端口。先核对当前编辑器的项目与实例 ID,启动调试后等待本次 runtime_mcp.state=ready,再调用 Runtime 工具;通常省略转发工具的 port。多开选择、启动参数继承和单次覆盖见 多开编辑器与 MCP 调试连接。
AI 助手调用方式
推荐优先使用编辑器 MCP 工具 runtime_call_tool。这个入口会由编辑器 MCP 转发到正在运行的客户端 Runtime TCP bridge。调用时显式传入客户端 runtime 工具名:
json
{
"name": "debug.ping",
"arguments": {}
}如果需要更长等待时间,可以增加 timeout_ms:
json
{
"name": "ui.snapshot",
"arguments": {
"maxDepth": 4,
"maxNodes": 200
},
"timeout_ms": 10000
}多人调试目标选择
AI 自动化的确定性匹配顺序、目标生命周期和错误恢复见 Runtime MCP 多人目标路由 Skill。本节保留协议字段和人工排查示例。
编辑器内嵌多人调试会在同一 Client 进程中创建多个 Gameplay/Wasmtime 实例。此时先调用外层 runtime_list_targets:
json
{
"timeout_ms": 5000
}目标列表位于返回 JSON 的 result.targets,每项包含短生命周期的 target_id(格式为 gameplay:<GameplayUUID>)、slot_id、字符串形式的 player_id、map_name、is_foreground 和 ready。只有一个目标时可继续省略 target_id;有多个目标时,runtime_list_tools 和 runtime_call_tool 都必须显式传入选定 ID:
json
{
"target_id": "gameplay:42",
"name": "ui.snapshot",
"arguments": {
"maxDepth": 4,
"maxNodes": 200
}
}AI 应按用户指定的模拟客户端、slot_id 或 player_id 选择目标;如果只有一个前台目标且它与任务一致,也可使用该目标。没有唯一匹配时应询问用户,不得静默选第一个。Gameplay 结束或重建后必须重新发现,不能复用旧 target_id。
当前第一阶段只保证 Runtime 请求进入正确的 Wasmtime 状态。input.* 仍通过进程级输入系统注入并受当前窗口/前台焦点影响,不保证多个嵌入视口可以同时独立接收输入。独立客户端进程自动取得各自端口,通过所属编辑器的发现记录选择连接;同进程的玩家仍使用 target_id 区分。
注意:debug.ping、debug.list_tools、ui.snapshot、ui.find 等脚本侧工具不会出现在外层 MCP tools/list 中。看不到它们是正常行为。外层 tools/list 至少需要能看到 runtime_call_tool;如果连它也看不到,说明当前 AI 没有连到带 Runtime 转发能力的编辑器 MCP。
如果 AI 客户端没有把 MCP 工具作为原生动作暴露出来,但可以访问完整编辑器的 HTTP 端口,优先使用项目生成的 Editor MCP helper:
powershell
ai/tools/Invoke-SceEditorMcp.ps1 -Context -Pretty
ai/tools/Invoke-SceEditorMcp.ps1 -ListTools -ToolPrefix runtime_ -Pretty
ai/tools/Invoke-SceEditorMcp.ps1 -Tool runtime_call_tool -ArgumentsJson '{"name":"ui.snapshot","arguments":{"maxDepth":4,"maxNodes":200},"timeout_ms":10000}' -Pretty这个 helper 会执行 MCP initialize / notifications/initialized,并默认拒绝把 TriggerMcpHost 当作完整编辑器。只有完整编辑器 MCP bridge 不可用,或正在专门诊断 direct TCP 时,才使用下面的 Runtime TCP fallback。
通过外层 Editor MCP 调用时,先把 tools/call.content[0].text 解析成 JSON。runtime_list_tools 以及 runtime_call_tool 调用 debug.list_tools 时,真正的 Runtime 返回体在 result 里;工具列表应读取 parsed.result.tools / parsed.result.count,不要读取顶层 parsed.tools:
js
const parsed = JSON.parse(contentText);
const runtime = parsed.result ?? parsed;
const tools = runtime.tools ?? [];这个 parsed.result ?? parsed 写法也兼容 direct TCP fallback,因为 fallback 直接返回 Runtime MCP payload。顶层 success 表示 Editor MCP bridge 转发是否成功;内层 runtime 工具的字段从 result 中读取。
direct TCP fallback
如果当前 AI 工具环境没有暴露 runtime_call_tool,可以直接调用项目内脚本:
powershell
ai/tools/Invoke-SceRuntimeMcp.ps1 -Port $runtimePort -RuntimeInstanceId $runtimeId -Ping -Wait
ai/tools/Invoke-SceRuntimeMcp.ps1 -Port $runtimePort -RuntimeInstanceId $runtimeId -ListTargets
ai/tools/Invoke-SceRuntimeMcp.ps1 -Port $runtimePort -RuntimeInstanceId $runtimeId -ListTools
ai/tools/Invoke-SceRuntimeMcp.ps1 -Port $runtimePort -RuntimeInstanceId $runtimeId -ListTools -TargetId gameplay:42
ai/tools/Invoke-SceRuntimeMcp.ps1 -Port $runtimePort -RuntimeInstanceId $runtimeId -Tool ui.snapshot -TargetId gameplay:42 -ArgumentsJson '{"maxDepth":4,"maxNodes":200}'项目内脚本是薄 shim,实际协议实现来自当前 WasiCoreSDK/tools/runtime-mcp。因此地图不会长期持有一份可漂移的 TCP/JSON 实现;重新生成 AI 上下文只会刷新入口。可用 ai/tools/Invoke-SceRuntimeMcp.ps1 -Port $runtimePort -RuntimeInstanceId $runtimeId -ClientVersion 查看共享客户端版本。
这个脚本只封装客户端 Runtime TCP bridge,不会启动编辑器 MCP,也不会出现在标准 MCP tools/list 中。它只用于 runtime_call_tool 缺失或诊断 Editor MCP 转发故障,不应与官方入口视为同等优先级。fallback 只允许连接 loopback 地址。
内置工具
debug.ping
用于确认 Runtime MCP 是否可用。
json
{
"name": "debug.ping",
"arguments": {}
}返回信息包括 protocol_version、runtime_tool_api_version、本次客户端进程唯一的 session_id、当前是否为客户端调试、是否为纯客户端调试、平台、项目路径和已注册工具数量。多次启停调试时可以用 session_id 判断响应是否来自新会话。
新版运行时还提供 startup_state.game_started 和 startup_state.loading_visible,分别表示框架已收到开局事件、框架启动加载页仍在显示。截图前应等待前者为 true、后者为 false,再留出渲染稳定时间;加载进度达到 100% 时仍可能显示加载页。该状态不包含项目自定义加载页或玩法是否可操作;旧运行时缺少字段时不能当作启动状态已验证。
debug.list_tools
返回当前客户端运行时内部已经注册的工具列表,用于区分“Runtime MCP 没连上”和“项目自定义工具没有注册”。
json
{
"name": "debug.list_tools",
"arguments": {}
}返回信息包括工具名、描述和输入参数 schema。项目注册了自定义工具后,可以先调用 debug.list_tools 确认工具是否已经进入运行时注册表。
debug.overlay 与 motor.snapshot
排查 PhysicalCharacter / KCC 的着地、台阶和碰撞问题时,先为目标 Gameplay 打开 MotorProbe。省略 unit_id 时由 Host 选择当前玩家英雄;显式传入时必须使用服务端 Unit ID。
json
{
"name": "debug.overlay",
"arguments": {
"mode": "MotorProbe",
"unit_id": 12345,
"snapshot_interval_ms": 33
}
}客户端为当前 Gameplay 保留最近 128 个权威 MotorProbe payload。随后读取最近帧:
json
{
"name": "motor.snapshot",
"arguments": {
"unit_id": 12345,
"max_frames": 32,
"after_physics_frame": 900
}
}| 参数 | 类型 | 说明 |
|---|---|---|
unit_id | int | 可选。服务端 Unit ID;省略时读取 Host 为本次订阅解析出的英雄。 |
max_frames | int | motor.snapshot 最多返回的最近帧数,范围 1 到 128,默认 32。 |
after_physics_frame | int | 只返回大于该物理帧编号的样本,适合增量轮询。已有样本均被过滤时仍返回 status=ok、count=0。 |
snapshot_interval_ms | int | MotorProbe 服务端采样间隔,范围 33 到 1000 毫秒,默认 100。 |
每个样本包含 logic_frame、physics_frame、server_time_ms、当前位置、上一物理步位置、速度、期望速度、胶囊半径/高度、台阶高度、最大坡度、grounded、碰撞 layer/mask,以及存在时的 selected_ground、forward_block、ignored_low_step、forward_sweep 和 penetration_contact。命中项包含点、法线、fraction、角色、深度或修正量。坐标直接来自服务端,不应与客户端插值位置混用。
motor_not_subscribed 表示尚未打开 MotorProbe;motor_target_mismatch 表示查询目标与本次订阅目标不同;motor_snapshot_pending 表示订阅已建立但首包尚未到达。没有权威样本时,motor_target_missing 和 motor_not_physical_character 只能根据客户端复制状态给出,会同时返回 classification_source=client_replica、classification_advisory=true 和 retryable=true;它们适合提示排查方向,不能作为服务端最终判定。旧客户端二进制会返回 motor_snapshot_native_unavailable,需要更新编辑器或目标平台客户端。
环形缓冲按 Gameplay 隔离;切换目标、修改采样间隔、关闭 overlay、退出或重建游戏局时会清空。多人调试应先通过 runtime_list_targets 选择正确的 target_id,再对同一目标调用上述两个工具。
debug.viewport_update
在支持此工具的新版编辑器中,AI 可以为选定 PIE 视口申请持续渲染租约。鼠标停在其它面板或编辑器在后台时,租约让目标视口继续绘制;不会改变 Ctrl+Z 等快捷键或桌面键鼠输入的归属。先用 runtime_list_tools 确认目标提供 debug.viewport_update。
json
{
"target_id": "gameplay:42",
"name": "debug.viewport_update",
"arguments": { "action": "acquire", "ttl_ms": 30000, "fps": 30 }
}保存返回的字符串 lease_id,在测试结束或取消时于 finally 中调用相同目标的 action=release;较长测试使用 action=renew 和 ttl_ms 在到期前续期。普通 MCP 调用返回后的连接关闭不会释放租约;调用方异常而未能释放时,租约按实际经过时间到期。退局、目标销毁或 Runtime bridge 停止也会清理租约。
| 参数 | 说明 |
|---|---|
action | status 查询状态,也是默认值;另支持 acquire、renew、release、request_frame。 |
lease_id | renew、release 必填,使用同一目标申请时返回的字符串。 |
ttl_ms | 申请或续期的有效毫秒数,1000~300000,默认 30000。 |
fps | 无焦点时请求的视口渲染帧率,1~60,默认 30;不修改游戏逻辑时间、暂停状态或正常前台帧率。 |
多个租约同时存在时采用最高请求帧率;释放只影响对应租约。request_frame 只请求一次刷新,不适合作为持续动画验收的替代。状态返回 engine_frame、目标视口最近完成绘制提交的 rendered_engine_frame、lease_count、continuous_fps、policy 和 blocked_reason。引擎帧推进不等于模型姿态已更新;验收仍应读取姿态或新画面。
后台运行要求目标视口及所属窗口可见且可绘制。最小化、隐藏、图形设备不可用等条件分别由 window_minimized、window_hidden、viewport_hidden、graphics_unavailable 等原因表示。租约不会自动切换多人页签或激活窗口。
持续动画测试建议按以下流程执行:
- 等待
runtime_list_targets返回就绪目标,再申请该目标的刷新租约;调试启动操作成功只表示已请求开局。 - 记录
rendered_engine_frame,执行操作后获取新帧截图,同时检查实际角色姿态或画面变化;游戏时间或动画计时增加不能证明画面已更新。 - 测试超过租约有效期时提前续期,结束后在
finally中释放。单次截图会临时请求刷新,不必单独申请持续租约;取消未完成的截图请求会清理其临时刷新需求。 - 遇到窗口最小化或隐藏时,恢复编辑器主窗口或显示目标页签后重试。嵌入视口也受编辑器主窗口可见性影响,租约不会绕过此限制。
debug.capture_screenshot
保存当前客户端画面为 PNG 文件。
json
{
"name": "debug.capture_screenshot",
"arguments": {
"path": "E:/Temp/ui-debug.png",
"overwrite": true,
"maxWidth": 1920,
"maxHeight": 1080
}
}参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
path | string | 可选。绝对路径或相对路径。不传时会保存到客户端默认截图目录。 |
overwrite | bool | 可选。目标文件已存在时是否覆盖。 |
maxWidth | int | 可选。输出图片最大宽度,按比例缩小,不会放大。 |
maxHeight | int | 可选。输出图片最大高度,按比例缩小,不会放大。 |
wait_for_new_frame | bool | 支持视口更新能力的 PIE 目标可用,默认 true。先请求并等待目标视口的新绘制提交;false 不等待。 |
frame_timeout_ms | int | 等待新绘制提交的上限,100~30000 毫秒,默认 3000;外层 timeout_ms 应留出额外的文件写入时间。 |
after_render_frame | int | 可选基准,等待目标视口的 rendered_engine_frame 大于该值,同时保证比截图请求前的已知绘制帧更新。 |
上述 PIE 截图能力以该目标 runtime_list_tools 返回的 schema 为准。截图保存选定视口所属的完整窗口,返回 capture_scope=target_window,不会裁剪为单独视口。fresh_frame=true 和 rendered_engine_frame 证明请求后已有目标视口的绘制提交;后者不是 GPU 读回的精确帧编号。最小化或隐藏目标会返回明确错误,等待超时返回 viewport_frame_timeout。不支持这些参数的旧运行时及独立客户端继续使用原截图接口。
截图请求在客户端内部仍按帧异步落盘,但调用方不需要自己轮询文件。Editor MCP runtime_call_tool 默认等待响应中的新文件写入完成,然后返回 pending=false 和 completion_state="completed";如确实只想提交请求,可传 wait_for_completion=false。direct TCP fallback 使用 -Wait 时也会先等待端口,再等待带 pending/path 的文件型结果完成。
若用于 AI 视觉检查,建议按需要传 maxWidth/maxHeight 限制到 1920x1080 或 1280x720,避免无谓的大图传输;如果需要检查细小文字或像素级细节,则不传这两个参数保留原始分辨率。
scenegraph.render_snapshot
只读采集当前主视口、相机、候选光照区域和灯光配置,适合排查全黑、雾遮挡、镜头裁剪或点光效果异常。先通过 debug.list_tools 确认运行时提供该工具;旧依赖库可继续使用截图与材质探测。
json
{
"name": "scenegraph.render_snapshot",
"arguments": {
"includeCurrentScenes": true,
"includeSession": true,
"includeReplicated": true,
"maxNodes": 1000,
"maxResults": 200,
"maxDiagnostics": 50
}
}query 可按节点名或组件类型子串筛选候选 Zone 和 Light。三项 include* 默认均为 true;主视口的 SceneGraph 始终纳入。maxNodes 范围为 1~20000,maxResults 与 maxDiagnostics 为 1~2000,默认分别为 1000、200、200。灯光和候选光照区域共用结果限额。
返回 viewport 的主相机、裁剪相机、视口矩形和 cluster 开关;相机包含位置、投影方式、裁剪距离、视角、宽高比和视图遮罩。矩形相对于该视口渲染目标,全零表示自动尺寸。遍历按层优先查看上层节点,减少复杂模型子树先耗尽额度而漏读场景灯光的情况。lights 包含类型、颜色、亮度、单位、范围、锥角、阴影与遮罩;同节点上的多个灯光分别返回。candidate_zones 包含优先级、是否包含相机位置、环境光、雾与暗角参数。对象同时保留图来源、节点路径和启用状态。
这些是配置证据,仍须结合实际截图核对。renderer_default_zone 表示渲染器默认配置,候选列表不能证明原生渲染实际选择了哪一个 Zone,因此 effective_camera_zone=null、zone_selection_status=unknown。暗角的 texture_attribute 仅是字符串观察值;原生 ResourceRef 的 Variant.ToString() 可能返回空串,直接设置 Texture 时也可能如此。空值标为 texture_attribute_status=unavailable,有字符串时标为 metadata_only,均不能证明遮罩存在或缺失。工具无法确认已解析的遮罩绑定或最终暗角效果,mask_resolved 与 effective_enabled 返回 null。
非有限数值返回 null;颜色使用现有字节颜色 getter。结果中的 truncated、diagnostics 与 diagnostics_truncated 表示遍历限额或部分读取失败,不能把缺失字段当作正常零值。Start-SceClientOnlyDebug.ps1 -Preview 会在工具可用时将这份配置存为 render.json,报告中的 renderEvidence 区分已采集、旧运行时不支持、发现失败与调用失败。
scenegraph.materials
列出当前客户端已加载 SCE 场景、脚本 session SceneGraph 与 replicated SceneGraph 中,StaticMeshComponent / CustomGeometryComponent 绑定的材质快照。适合排查模型不可见、材质路径不对、RenderPath pass、纹理槽、顶点布局、Stencil/RenderOrder/CullMode 等问题。
json
{
"name": "scenegraph.materials",
"arguments": {
"query": "BlackHole",
"includeCurrentScenes": true,
"includeSession": true,
"includeReplicated": true,
"maxResults": 20,
"passNames": ["base", "alpha", "depth", "singlelayerwater"]
}
}参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
query | string | 可选。按节点名、材质类型名或 technique path 子串过滤。 |
includeCurrentScenes | bool | 是否包含当前已加载的 SCE 场景,默认 true。 |
includeSession | bool | 是否包含客户端脚本本地创建的 session SceneGraph。默认 true。 |
includeReplicated | bool | 是否包含客户端订阅到的 replicated SceneGraph。默认 true。 |
maxNodes | int | 最多遍历节点数。默认 1000。 |
maxResults | int | 最大返回材质条目数。默认 200。 |
maxDiagnostics | int | 瞬态资源不可读时最多返回的诊断数,默认跟随 maxResults。 |
passNames | string[] | 需要检查的 pass 名。常用 ["base","alpha","depth","singlelayerwater"]。 |
返回条目包含 graph.source/sources/scenes、node、通用 drawable、material 和组件专用字段。current_scene 表示当前 SCE 场景;同一底层图关联多个 SCE 场景时会聚合到 graph.scenes。StaticMesh 保留原有 static_mesh 字段,并增加 Mesh、LOD0 Geometry、VertexBuffer/IndexBuffer 元数据;CustomGeometry 返回 dynamic、geometry/material slot 与顶点数。当前公开 API 尚不能读取 CustomGeometry 提交后的精确 GPU layout,因此会明确返回 layout_status=unavailable,不会根据脚本调用猜测。
material.textures 只列非空常用纹理槽,包含具体纹理类型、逻辑资源名、尺寸、数字 format、components、levels、压缩、Usage、sRGB、过滤、寻址、各向异性和 multisample;不会回读像素。material.passes 除 pass 是否存在外,还返回实际 blend/depth/alpha-test、shader、effective defines 和 loaded frame。共享 Material 在单次请求内只采集一次这些深层元数据。瞬态释放或场景切换造成的局部读取失败进入有界 diagnostics / status=unavailable,不会中止其他结果。旧字段 shader_path / has_shader 继续保留。
material.probe
返回第一个匹配的 StaticMesh 或 CustomGeometry 材质快照。已知节点名、节点 ID 或材质槽时优先用它,比 scenegraph.materials 更适合单点核对。
json
{
"name": "material.probe",
"arguments": {
"query": "BlackHole",
"materialIndex": 0,
"includeSession": true,
"passNames": ["base", "alpha", "depth", "singlelayerwater"]
}
}参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
nodeId | int | 可选。精确匹配 SceneGraph 节点 ID。 |
query | string | 可选。按节点名、材质类型名或 technique path 子串过滤。 |
materialIndex | int | 可选。指定 StaticMesh 或 CustomGeometry 的几何体/材质槽索引。 |
includeCurrentScenes | bool | 是否包含当前已加载的 SCE 场景,默认 true。 |
includeSession | bool | 是否包含客户端脚本本地创建的 session SceneGraph。默认 true。 |
includeReplicated | bool | 是否包含客户端订阅到的 replicated SceneGraph。默认 true。 |
maxNodes | int | 最多遍历节点数。默认 1000。 |
maxDiagnostics | int | 最多返回的局部不可用诊断,默认 50。 |
passNames | string[] | 需要检查的 pass 名。 |
如果没有匹配项,工具返回 material_not_found;若目标 slot 在场景切换等瞬态中无法读取,则返回 entry=null 和与目标相关的 diagnostics。如果 technique_path 看起来是全小写,这是引擎资源路径归一化导致的,和 KnownTechniquePath 对照时按大小写不敏感比较。
ui.snapshot
返回当前 GameUI 控件树快照。
json
{
"name": "ui.snapshot",
"arguments": {
"includeInvisible": false,
"maxDepth": 6,
"maxNodes": 500
}
}参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
includeInvisible | bool | 是否包含当前不可见控件。默认 false。 |
maxDepth | int | 返回的最大层级深度。默认 12。 |
maxNodes | int | 返回的最大节点数量。默认 500。 |
返回节点包含 id、type、name、text、path、visible、actually_visible、disabled、child_count 和 rect_px 等字段。Label 节点还会返回 rich_text_spans;每项包含 text、link_id 和一个或多个最终排版后的 rects_px,可直接用于链接点击验证。
这些坐标统一使用当前游戏 UI 的屏幕像素坐标系。无论客户端嵌在编辑器 GameView 中,还是以纯客户端独立窗口运行,传给 ui.click / input.pointer 的都应是这里返回的坐标;不要自行叠加编辑器外框偏移。若截图调用传了 maxWidth / maxHeight 并发生缩放,也不要直接把缩放后 PNG 上的像素坐标当作输入坐标。
ui.find
按 id、名称、文本或类型查找控件。
json
{
"name": "ui.find",
"arguments": {
"query": "按钮1",
"field": "text",
"exact": true,
"maxResults": 20
}
}参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
id | integer | 精确匹配运行时控件 id。 |
query | string | 查询文本。 |
field | string | any、id、name、text 或 type。默认 any。 |
exact | bool | 是否精确匹配。默认 false。 |
includeInvisible | bool | 是否包含不可见控件。默认 false。 |
maxResults | int | 最大返回数量。默认 50。 |
ui.get_rect
返回单个控件的屏幕像素坐标和尺寸。
json
{
"name": "ui.get_rect",
"arguments": {
"query": "按钮1",
"field": "text"
}
}如果已经从 ui.snapshot 或 ui.find 得到了控件 id,推荐用 id 查询:
json
{
"name": "ui.get_rect",
"arguments": {
"id": 123456789
}
}返回的 rect_px 包含:
json
{
"x": 888,
"y": 931,
"width": 75,
"height": 40,
"center_x": 925,
"center_y": 951
}ui.click
通过客户端内部输入路径发送一次点击。可以直接传屏幕像素坐标,也可以传控件 id 点击中心;对于富文本 Label,推荐同时传 id 和 linkId,工具会使用 rich_text_spans 中的实际命中矩形。
json
{
"name": "ui.click",
"arguments": {
"id": 123456789,
"linkId": "open_shop"
}
}参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
x / y | integer | 屏幕像素坐标,必须成对提供。 |
id | integer | 控件 id;没有 linkId 时点击控件中心。 |
linkId | string | Label 的链接 id,必须与 id 一起使用。 |
button | integer | 1 左键、2 中键、4 右键,默认 1。 |
includeInvisible | bool | 是否允许按 id 匹配不可见控件,默认 false。 |
工具返回 queued=true 表示点击已排入客户端主线程。随后再次调用 ui.snapshot 或检查日志来确认业务事件产生的状态变化。
input.pointer / input.key / input.text / input.reset
需要验证悬停、按下状态、拖拽、滚轮、快捷键或文本输入时,使用状态化的 input.* 工具。ui.click 仍是普通控件点击的首选;底层输入工具用于必须拆分 move/down/up 的交互。
input.pointer
input.pointer 通过正常引擎输入路径发送指针动作。坐标与 ui.snapshot、ui.find、ui.get_rect 返回的游戏 UI 像素坐标一致。
| 参数 | 类型 | 说明 |
|---|---|---|
action | string | 必填。move、down、up 或 wheel。 |
x / y | integer | 必须成对提供且不能为负。move 必填;其它动作可选,提供时会先移动再执行动作。 |
button | integer | down / up 使用。1 左键、2 中键、4 右键,默认 1。 |
delta | integer | wheel 必填,且不能为 0。正负方向遵循当前客户端平台的引擎滚轮语义。 |
普通拆分点击:
json
{ "name": "input.pointer", "arguments": { "action": "move", "x": 300, "y": 240 } }
{ "name": "input.pointer", "arguments": { "action": "down", "button": 1 } }
{ "name": "input.pointer", "arguments": { "action": "up", "button": 1 } }拖拽示例:
json
{ "name": "input.pointer", "arguments": { "action": "move", "x": 300, "y": 240 } }
{ "name": "input.pointer", "arguments": { "action": "down", "button": 1 } }
{ "name": "input.pointer", "arguments": { "action": "move", "x": 520, "y": 360 } }
{ "name": "input.pointer", "arguments": { "action": "up", "button": 1 } }如果目标可能在动作之间移动,可以在 down / up 中再次提供 x / y。up 只释放 Runtime MCP 自己持有的按钮,不会释放用户在物理设备上按住的按钮。
滚轮示例:
json
{ "name": "input.pointer", "arguments": { "action": "wheel", "x": 520, "y": 360, "delta": -1 } }input.key
| 参数 | 类型 | 说明 |
|---|---|---|
action | string | 必填。down、up 或原子操作 press。 |
key | string | 必填。SDL/Urho 键名,例如 A、Enter、Escape、LeftCtrl、F5、ArrowUp。 |
modifiers | string[] | 仅 press 支持。可选值为 ctrl、shift、alt、meta。 |
持续按键需要显式配对:
json
{ "name": "input.key", "arguments": { "action": "down", "key": "ArrowUp" } }
{ "name": "input.key", "arguments": { "action": "up", "key": "ArrowUp" } }组合键使用 press。它会临时按下调用尚未持有的修饰键,并只释放本次调用自己取得的修饰键:
json
{ "name": "input.key", "arguments": { "action": "press", "key": "S", "modifiers": ["ctrl"] } }input.text
input.text 的 text 为必填字符串,长度为 1 至 4096。它发送一个 Unicode E_TEXTINPUT 载荷,不会把文字拆成键码,也不会代替 Enter、Escape、方向键或快捷键。
先用 ui.find 找到输入框,再用 ui.click 让它获得焦点,最后输入文字:
json
{ "name": "ui.click", "arguments": { "id": 123456789 } }
{ "name": "input.text", "arguments": { "text": "测试文本" } }没有焦点输入控件时,queued=true 只表示文本事件已送入引擎,不代表任何业务控件接收或保存了文字。
GameUI Input 默认可能在失去焦点时才把编辑中的文字提交到控件 Text 属性。因此输入框仍聚焦时,ui.snapshot 的 text 可能还是上一次提交值;此时先检查 OnInputTextChanged 业务事件,或用 ui.click 把焦点移到其它控件后再读取快照。
input.reset
如果自动化在 down 之后中断,或无法确认某个键/按钮是否已配对释放,调用:
json
{ "name": "input.reset", "arguments": {} }它会释放 Runtime MCP 自己持有的键和指针按钮,并结束 synthetic pointer position 覆盖;不会无差别释放用户的物理输入。多步输入序列应把 reset 作为失败清理路径,而不是只在成功路径调用。
所有输入工具返回 queued=true 都只表示动作进入客户端主线程队列。应再次调用 ui.snapshot、读取输入框文本、检查项目事件日志或保存截图,确认业务结果后再判定成功。
注册项目自定义工具
项目可以在客户端调试代码中注册自己的 runtime 工具。注册代码必须放在客户端编译分支中。
csharp
#if CLIENT
using GameUI.Debug;
using System.Text.Json.Nodes;
public static class ProjectRuntimeDebugTools
{
public static void Register()
{
RuntimeDebugToolRegistry.Register(
"project.get_preview_state",
"Return current local preview state.",
new JsonObject
{
["type"] = "object",
["properties"] = new JsonObject
{
["includeItems"] = new JsonObject { ["type"] = "boolean" }
}
},
arguments =>
{
var includeItems = arguments?["includeItems"]?.GetValue<bool>() ?? false;
return new JsonObject
{
["mode"] = Game.IsClientOnly ? "client_only" : "debug",
["include_items"] = includeItems,
["time"] = DateTimeOffset.Now.ToString("O")
};
});
}
}
#endif在客户端初始化阶段调用一次:
csharp
#if CLIENT
ProjectRuntimeDebugTools.Register();
#endifAI 助手即可通过 runtime_call_tool 调用:
json
{
"name": "project.get_preview_state",
"arguments": {
"includeItems": true
}
}建议使用带命名空间的工具名,例如 project.xxx、ui.xxx、debug.xxx。不要注册任意 eval 工具作为默认能力;如果项目临时需要执行脚本,应只在本地调试分支中显式注册,并清楚它的风险。
返回格式和错误处理
成功响应通常是:
json
{
"success": true,
"result": {
"tool": "debug.ping"
},
"id": 1
}失败响应通常是:
json
{
"success": false,
"error_code": "tool_not_found",
"message": "Runtime tool not found: project.unknown",
"id": 1
}项目自定义工具可以抛出 RuntimeDebugToolException 返回明确错误码:
csharp
#if CLIENT
throw new RuntimeDebugToolException(
"preview_state_not_ready",
"Preview state is not ready. Try again after the next frame.");
#endif普通异常也会被包装成结构化错误,但更推荐使用明确错误码,便于 AI 助手判断是否重试。
常见问题
AI 助手提示 Client runtime MCP is not reachable
通常表示当前没有正在运行的调试客户端,或客户端还没有完成启动。请先通过编辑器启动普通调试、调试不编译或纯客户端调试,再调用 runtime 工具。
如果是通过 ai/tools/Start-SceClientOnlyDebug.ps1 直接启动客户端,确认启动前已经把最新 GameEntry.dll 部署到 AppBundle。dotnet build 只会刷新 src/bin/.../GameEntry.dll,不会自动复制到 AppBundle/managed 或 ui/AppBundle/managed。编辑器内点击调试会自动执行这一步,直接拉起客户端不会。
tools/list 中看不到 runtime_call_tool
runtime_call_tool 是编辑器 MCP 的外层桥接工具。如果它不存在,优先检查当前 AI 是否真的连到了完整 SCE Editor MCP,而不是 TriggerMcpHost 或其它独立 Host。
当前 Runtime bridge 本身不是标准 MCP tools/list 端点。必要时可以用项目内脚本直接验证客户端 bridge 是否在线:
powershell
ai/tools/Invoke-SceRuntimeMcp.ps1 -Port $runtimePort -RuntimeInstanceId $runtimeId -Ping -Wait
ai/tools/Invoke-SceRuntimeMcp.ps1 -Port $runtimePort -RuntimeInstanceId $runtimeId -Tool debug.capture_screenshot -ArgumentsJson '{"path":"RuntimeMcpScreenshots/ui.png","overwrite":true,"maxWidth":1280,"maxHeight":720}' -Wait$runtimePort 和 $runtimeId 必须来自本次启动的连接结果(editor_get_context.mcp.runtime 或启动 helper 的 runtimeMcp)。等价的底层 TCP smoke test 如下:
powershell
$client = [System.Net.Sockets.TcpClient]::new("127.0.0.1", $runtimePort)
$stream = $client.GetStream()
$request = @{ id=1; method='runtime.call_tool'; params=@{name='debug.ping'; arguments=@{}; runtime_instance_id=$runtimeId} } | ConvertTo-Json -Depth 10 -Compress
$request += "|*|`n"
$bytes = [System.Text.Encoding]::UTF8.GetBytes($request)
$stream.Write($bytes, 0, $bytes.Length)
$buffer = New-Object byte[] 4096
$read = $stream.Read($buffer, 0, $buffer.Length)
[System.Text.Encoding]::UTF8.GetString($buffer, 0, $read)
$client.Dispose()如果本次发现的实际端口没有监听,先检查调试状态、客户端进程和实例 ID,并重新取得连接信息。如果 direct TCP 能返回 debug.ping,但外层没有 runtime_call_tool,问题在 AI 与编辑器 MCP 的连接层。
tools/list 中看不到 ui.snapshot
这是预期行为。脚本侧工具不会进入外层 MCP 工具列表。请通过 runtime_call_tool 调用 ui.snapshot。
ui.snapshot 返回节点为空
常见原因是 UI 还没有创建完成,或当前项目使用 Canvas 直接绘制而不是 GameUI 控件树。可以让 AI 助手等待几帧后重试,或改用 debug.capture_screenshot 先确认画面内容。
input.pointer 已排队但没有命中控件
先重新调用 ui.snapshot 或 ui.get_rect,确认控件仍然可见、可交互且坐标没有因布局变化而过期。输入坐标必须直接使用 Runtime MCP 返回的游戏 UI rect_px;不要叠加编辑器窗口边框或 GameView 偏移,也不要使用经过 maxWidth / maxHeight 缩放后的截图坐标。
然后检查业务证据:控件文本是否变化、pointer/link 事件是否写入日志、截图是否出现按下或焦点状态。queued=true 本身不证明命中。若序列曾在 down 后中断,先调用 input.reset 再重试。
截图返回成功但文件还没出现
截图由客户端渲染线程按帧异步保存。官方 runtime_call_tool 默认等待文件完成;fallback 请使用 -Wait。只有显式设置 wait_for_completion=false 或 fallback 不传 -Wait 时,才会看到原始 pending=true,此时调用方自行承担完成检测。
fallback 返回 runtime_mcp_fallback_failed
fallback 的本地失败会返回结构化 JSON,其中 stage 指出失败层级:argument_validation、connect、write、read、response_parse、completion_wait 或 client_resolution。优先根据 stage 排查,不要把 JSON 参数错误误判成 runtime 工具或截图实现错误。
自定义工具注册后调用不到
请确认注册代码在 #if CLIENT 分支中执行,并且执行时客户端处于调试模式。也可以先调用 debug.ping 查看 registered_tool_count,再调用 debug.list_tools 查看具体工具名是否已经注册。
和纯客户端调试配合
纯客户端调试适合快速验证 UI 和本地表现逻辑。启动方式见 纯客户端调试指南。
在纯客户端调试中,debug.ping 会返回 client_only=true。此时不要访问 Entity、Unit、CloudData 等服务端能力;项目自定义 runtime 工具也应只读取客户端可用状态。