Skip to content

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_idint可选。服务端 Unit ID;省略时读取 Host 为本次订阅解析出的英雄。
max_framesintmotor.snapshot 最多返回的最近帧数,范围 1 到 128,默认 32。
after_physics_frameint只返回大于该物理帧编号的样本,适合增量轮询。已有样本均被过滤时仍返回 status=ok、count=0。
snapshot_interval_msintMotorProbe 服务端采样间隔,范围 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 停止也会清理租约。

参数说明
actionstatus 查询状态,也是默认值;另支持 acquire、renew、release、request_frame。
lease_idrenew、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 等原因表示。租约不会自动切换多人页签或激活窗口。

持续动画测试建议按以下流程执行:

  1. 等待 runtime_list_targets 返回就绪目标,再申请该目标的刷新租约;调试启动操作成功只表示已请求开局。
  2. 记录 rendered_engine_frame,执行操作后获取新帧截图,同时检查实际角色姿态或画面变化;游戏时间或动画计时增加不能证明画面已更新。
  3. 测试超过租约有效期时提前续期,结束后在 finally 中释放。单次截图会临时请求刷新,不必单独申请持续租约;取消未完成的截图请求会清理其临时刷新需求。
  4. 遇到窗口最小化或隐藏时,恢复编辑器主窗口或显示目标页签后重试。嵌入视口也受编辑器主窗口可见性影响,租约不会绕过此限制。

debug.capture_screenshot ​

保存当前客户端画面为 PNG 文件。

json
{
  "name": "debug.capture_screenshot",
  "arguments": {
    "path": "E:/Temp/ui-debug.png",
    "overwrite": true,
    "maxWidth": 1920,
    "maxHeight": 1080
  }
}

参数说明:

参数类型说明
pathstring可选。绝对路径或相对路径。不传时会保存到客户端默认截图目录。
overwritebool可选。目标文件已存在时是否覆盖。
maxWidthint可选。输出图片最大宽度,按比例缩小,不会放大。
maxHeightint可选。输出图片最大高度,按比例缩小,不会放大。
wait_for_new_framebool支持视口更新能力的 PIE 目标可用,默认 true。先请求并等待目标视口的新绘制提交;false 不等待。
frame_timeout_msint等待新绘制提交的上限,100~30000 毫秒,默认 3000;外层 timeout_ms 应留出额外的文件写入时间。
after_render_frameint可选基准,等待目标视口的 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"]
  }
}

参数说明:

参数类型说明
querystring可选。按节点名、材质类型名或 technique path 子串过滤。
includeCurrentScenesbool是否包含当前已加载的 SCE 场景,默认 true。
includeSessionbool是否包含客户端脚本本地创建的 session SceneGraph。默认 true。
includeReplicatedbool是否包含客户端订阅到的 replicated SceneGraph。默认 true。
maxNodesint最多遍历节点数。默认 1000。
maxResultsint最大返回材质条目数。默认 200。
maxDiagnosticsint瞬态资源不可读时最多返回的诊断数,默认跟随 maxResults。
passNamesstring[]需要检查的 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"]
  }
}

参数说明:

参数类型说明
nodeIdint可选。精确匹配 SceneGraph 节点 ID。
querystring可选。按节点名、材质类型名或 technique path 子串过滤。
materialIndexint可选。指定 StaticMesh 或 CustomGeometry 的几何体/材质槽索引。
includeCurrentScenesbool是否包含当前已加载的 SCE 场景,默认 true。
includeSessionbool是否包含客户端脚本本地创建的 session SceneGraph。默认 true。
includeReplicatedbool是否包含客户端订阅到的 replicated SceneGraph。默认 true。
maxNodesint最多遍历节点数。默认 1000。
maxDiagnosticsint最多返回的局部不可用诊断,默认 50。
passNamesstring[]需要检查的 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
  }
}

参数说明:

参数类型说明
includeInvisiblebool是否包含当前不可见控件。默认 false。
maxDepthint返回的最大层级深度。默认 12。
maxNodesint返回的最大节点数量。默认 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
  }
}

参数说明:

参数类型说明
idinteger精确匹配运行时控件 id。
querystring查询文本。
fieldstringany、id、name、text 或 type。默认 any。
exactbool是否精确匹配。默认 false。
includeInvisiblebool是否包含不可见控件。默认 false。
maxResultsint最大返回数量。默认 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 / yinteger屏幕像素坐标,必须成对提供。
idinteger控件 id;没有 linkId 时点击控件中心。
linkIdstringLabel 的链接 id,必须与 id 一起使用。
buttoninteger1 左键、2 中键、4 右键,默认 1。
includeInvisiblebool是否允许按 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 像素坐标一致。

参数类型说明
actionstring必填。move、down、up 或 wheel。
x / yinteger必须成对提供且不能为负。move 必填;其它动作可选,提供时会先移动再执行动作。
buttonintegerdown / up 使用。1 左键、2 中键、4 右键,默认 1。
deltaintegerwheel 必填,且不能为 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 ​

参数类型说明
actionstring必填。down、up 或原子操作 press。
keystring必填。SDL/Urho 键名,例如 A、Enter、Escape、LeftCtrl、F5、ArrowUp。
modifiersstring[]仅 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();
#endif

AI 助手即可通过 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 工具也应只读取客户端可用状态。