把 Claude Code 和 UE5.8 组合到一起做游戏开发时,最容易被卡住的环节往往不是 C++ 还是 Blueprint 的选择,而是 Claude 拿不到引擎状态的实时反馈:编辑器日志在哪里、控制台命令能不能执行、某个 Python 脚本能不能跑、蓝图层级和资产列表长什么样。MCP(Model Context Protocol)解决的就是这个跨进程连接问题。它让 Claude Code 通过一套标准协议去调用 UE5.8 的编辑器能力,而不是只靠对话里粘贴报错片段,再让开发者手动去执行命令。
本文会从概念、环境准备、MCP Server 实现、Claude Code 接线、真实 UE 工作流、排错和工程化落地几个方向,完成一套完整的 UE5.8 MCP 配置与实战。读完并跟着做完之后,你会得到一个本地运行的 MCP Server,可以在 Claude Code 中读取项目日志、执行白名单内的编辑器命令、调用项目内固定的 UE Python 脚本。这套结构可以复用到实际游戏项目中,也可以作为你后续扩展资产生成、日志分析和 Profiling 工具的基础。
1. Claude Code 与 UE5.8 的衔接,难点出在工具边界上
1.1 MCP 在 UE 场景中解决了什么
Claude Code 本身是一个跑在终端里的编码智能体。它能看到项目目录、能修改文件、能执行命令,但它默认看不到 UE5.8 编辑器内部的运行时状态。编辑器崩溃后产生的日志,Claude Code 可以读取文件内容,但它不知道这条日志对应什么操作;编辑器刚发出的警告,Claude Code 也没有主动感知的通道;你要执行一次stat fps或跑一个资产转换脚本,Claude Code 同样缺少一个统一的调用入口。
如果只用命令行,很多人会想到让 Claude 直接通过child_process去启动UnrealEditor-Cmd.exe。这确实能做事,但风险很高:任何自然语言描述都可能变成一段不可控的参数,引擎启动失败、命令名写错、脚本路径越界,这些情况都会让一个看似合理的操作变成破坏性操作。
MCP 的解决方案是把外部系统包装成一组有明确 schema 的工具。Claude Code 不能随意拼接可执行命令,而是只能调用 MCP Server 暴露出的固定工具。工具的参数、范围、执行逻辑都由开发者定义。这样既保留了 Claude Code 的自动化能力,又把边界控制在了工具实现这一层。
1.2 Skill 与 MCP Server 的边界
很多人看到 Claude Code 里既能配置 Skill,又能配置 MCP Server,容易把它们搞混。Skill 本质上是一套给 Agent 看的工作流说明:它告诉 Claude 遇到某类任务时应该先做什么、再做什么、最后产出什么格式的结果。Skill 本身不主动连接外部系统,它只是在增强 Claude 的“思考方式”。
MCP Server 则是真正执行外部操作的服务。它负责和 UE5.8、数据库、浏览器工具、设计软件这类外部进程通信。MCP Server 暴露出的工具可以读取日志、执行命令、调用脚本,也可能对系统产生实际影响。所以 Skill 偏向于知识和工作流,MCP Server 偏向于动作和执行。
用一张表可以看得很清楚:
| 维度 | Skill | MCP Server |
|---|---|---|
| 核心作用 | 规范任务拆解、给 Claude 注入领域知识 | 把外部系统能力包装成可调用工具 |
| 是否跨进程 | 否,只在 Claude Code 内部生效 | 是,需要独立进程或外部服务 |
| 是否产生副作用 | 一般不产生系统副作用 | 可能执行命令、修改文件、操作引擎 |
| 适用场景 | 代码审查规范、任务拆解策略、引擎术语指导 | 读取 UE 日志、执行编辑器命令、运行 UE Python 脚本 |
| 风险控制 | 低,主要是 prompt 层面的影响 | 高,必须做白名单、路径约束和权限控制 |
在实际项目中,两者的正确关系是配合使用:Skill 负责告诉 Claude“接到 UE 崩溃排查任务时,先看 Saved/Logs 下对应日志,再分析堆栈,最后给出修复建议”;MCP Server 负责把“读取日志末尾 N 行”这个动作切实执行掉。
1.3 本文目标:可运行的本地工具闭环
这篇文章的目标不是做一个看起来很宏大的“全自动 UE 开发平台”,而是先建立一条最小但完整的链路:Claude Code 发起工具调用,MCP Server 接收请求,UE 桥接层执行受限操作,结果再返回给 Claude Code。想在这个链路上扩展其他 UE 能力时,只需要新增工具和对应的桥接函数。
为了不让整篇教程停留在“配置成功”的层面,我会把这条链路落实到三个可以反复验证的场景:
- 查看项目日志最后 N 行,用于定位崩溃和报错。
- 在 UE 编辑器批处理模式下执行白名单内的 Console 命令。
- 运行项目脚本目录内固定的 UE Python 脚本。
这三个场景都不需要把 UE 编辑器立刻改成插件架构,适合先用脚本式桥接验证 MCP 是否打通。真正进入生产环境后再考虑用 UE 插件、服务进程和权限体系增强稳定性。
2. 先用最小成本准备好运行环境
2.1 环境清单与版本确认
在动手写 MCP Server 之前,先把环境分成三层确认:基础运行层、Claude Code 层、UE 项目层。这三层的版本不一致,后面出现的问题会把排查方向引向错误的地方。
| 组件 | 建议要求 | 用途 |
|---|---|---|
| Node.js | 18 或更高 | 运行 MCP Server,以及 Claude Code 的常见安装方式 |
| npm | 9 或更高 | 安装 SDK 和依赖 |
| Claude Code | 当前最新版 | 作为 MCP 客户端 |
| Unreal Engine | 5.8 或当前项目正在使用的 UE5 版本 | 提供 UnrealEditor-Cmd 可执行文件和日志 |
| UE 项目 | 任意可打开的项目 | 作为被调试对象 |
先执行三个命令确认基础层的版本:
node -v npm -v claude --version如果claude命令还不存在,说明 Claude Code 还没有安装。如果node版本过低,先升级 Node 再继续,因为新版 MCP SDK 对 Node 版本有明确要求。
2.2 安装 Claude Code 并确认命令行可用
Claude Code 的安装方式会随版本更新而调整。目前常见的有两种方式:npm 全局安装和官方安装脚本。下面给出 npm 方式作为示例,实际安装时以你的系统环境和当前文档为准。
npm install -g @anthropic-ai/claude-code安装完成后,重新打开一个终端,执行:
claude --version这个命令能正常输出版本号,表示 CLI 已经进入 PATH。这一步很关键,因为后面 MCP Server 通过 stdio 传输启动时,Claude Code 需要在自己的进程环境里找到可执行程序。如果你是在桌面图形界面里启动的 Claude Code,而 Node 只存在于某个用户级 PATH 中,很容易出现“工具能配置但启动失败”的情况。
2.3 让 UE5.8 项目具备脚本入口
UE5.8 默认支持 Python 插件,但在编辑器里调用 Python 前需要确认插件处于启用状态。打开 UE 编辑器,进入 Edit > Plugins,搜索 Python Editor Script Plugin,确认它已启用。不同的 UE 版本插件名称和入口位置可能微调,但核心能力一致:它允许编辑器在运行时执行 Python 脚本,也能在引擎启动时通过命令行参数执行脚本。
还需要确认两个路径,后面配置.mcp.json时会用到:
- UE 项目文件路径,例如
D:/UnrealProjects/MyGame/MyGame.uproject。 UnrealEditor-Cmd.exe的路径,通常位于引擎安装目录下,例如C:/Program Files/Epic Games/UE_5.8/Engine/Binaries/Win64/UnrealEditor-Cmd.exe。
在 PowerShell 或 Git Bash 中确认一下:
ls "C:/Program Files/Epic Games/UE_5.8/Engine/Binaries/Win64/UnrealEditor-Cmd.exe"日志位置一般在项目目录:
<项目根目录>/Saved/Logs/<项目名>.log比如D:/UnrealProjects/MyGame/Saved/Logs/MyGame.log。这是后面读取日志工具要指向的路径。
3. 理清 MCP 的通信模型,再写服务器
3.1 三种核心能力:Tool、Resource、Prompt
MCP 协议并不是只提供“工具”这一种能力。它定义了三种可以让客户端消费的资源类型:
Tool 是最常用的一种,本质是一个可被调用的函数。Claude Code 看到 Tool 的 name、description 和 inputSchema 之后,会决定什么时候调用、传什么参数。UE 场景里,读取日志、执行命令、运行脚本都适合做成 Tool。
Resource 提供的是“按地址读取内容”的能力,类似于文件读取接口。比如引擎日志、配置文件、项目结构清单,都可以做成 Resource。Claude 不需要主动执行命令,只需要按 URI 去读取内容。
Prompt 描述的是模板化提示词,用于引导 Agent 在特定场景下如何使用其他工具。它不执行代码,只提供交互模板。
作为第一阶段,优先实现 Tool 就足够。Resource 和 Prompt 可以在链路稳定后再补。
3.2 stdio 与 HTTP 传输的选择
MCP Server 的传输方式直接影响部署结构。Claude Code 最常用的是 stdio 传输:Claude Code 启动 MCP Server 子进程,通过标准输入和标准输出与它通信。这种方式不需要监听端口,也不需要处理跨域和认证,适合本机开发工具。
HTTP 或 SSE 传输适合把 MCP Server 部署成独立服务,让多个客户端或多人共享一个引擎能力网关。但引入网络层之后,必须额外考虑端口暴露、认证、访问控制和日志审计。
| 传输方式 | 适合场景 | 主要注意事项 |
|---|---|---|
| stdio | Claude Code 本地调用 | 必须能找到 node 等可执行文件路径 |
| HTTP / SSE | 团队共享、独立服务 | 需要认证、IP 白名单、端口安全管理 |
本文的 UE 场景默认采用 stdio。原因很简单:UE 编辑器操作通常绑定在某台开发机上,启动一个常驻 HTTP 服务会扩大攻击面,而 stdio 子进程的生命周期跟随 Claude Code,不容易被外部扫描。
3.3 安全边界:白名单、路径约束、人工复核
UE 编辑器拥有很大的权限,它可以操作资产、执行命令、运行 Python 代码,甚至会触达项目外的目录。因此 MCP Server 不能做成“万能命令转发器”。必须定义三个基础安全边界。
第一是命令白名单。Console 命令不允许任意传入,只能从固定集合中选择。这样即使大模型产生了一个看起来很合理的命令,只要不在白名单里,就不会被执行。
第二是脚本路径约束。UE Python 脚本只能位于项目内部的固定目录,例如MCP/scripts。通过路径解码和规范化检查,避免..和绝对路径绕过。
第三是人工复核。Claude Code 在执行工具前通常会让用户确认,但这个机制不能作为唯一防线。MCP Server 内部也要有自己的日志和审计,记录谁在什么时候调用了哪些工具、返回了什么结果。
注意:路径白名单只是第一层防线。真正上线前,白名单本身也要由团队评审,不能完全依赖某个工具自动生成。
4. 实现一个可运行的 UE5.8 MCP Server
4.1 初始化 Node 项目并安装 MCP SDK
创建一个项目目录:
mkdir -p D:/ue58-mcp/server cd D:/ue58-mcp/server npm init -y安装 MCP SDK:
npm install @modelcontextprotocol/sdkSDK 提供 Server 基础类、stdio 传输和协议类型。还需要用到 Node 内置模块child_process、fs、path,这些不需要额外安装。
最终结构大致如下:
ue58-mcp/ server/ package.json index.mjs ue-bridge.mjs scripts/ list_assets.py check_log.py .mcp.jsonscripts目录放 UE Python 脚本,server目录放 MCP Server,项目根目录放 Claude Code 的 MCP 配置。
4.2 注册三个 UE 工具
先想清楚每个工具的输入和输出,再写代码。避免做一个“万能工具”,因为万能工具等于没有边界。
| 工具名 | 输入 | 输出 | 安全控制 |
|---|---|---|---|
| ue58_tail_log | lines 数字 | 日志末尾文本 | 只读,限制最大行数 |
| ue58_run_console | command 字符串 | stdout/stderr | 白名单精确匹配 |
| ue58_run_python | script 相对路径 | stdout/stderr | 路径限定在项目脚本目录 |
这里有一个容易被忽略的原则:工具描述里要写清楚“什么时候用、什么时候不用”。比如ue58_run_console只接受白名单内命令,描述里就要明确提示 Claude 不要传入白名单之外的命令,否则会返回拒绝信息。
4.3 Server 入口 index.mjs
下面代码是一个可直接运行的 MCP Server 入口:
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { runUeCommand, runUePython, readProjectLog } from "./ue-bridge.mjs"; const server = new Server( { name: "ue58-mcp", version: "0.1.0", }, { capabilities: { tools: {} }, } ); const TOOLS = [ { name: "ue58_tail_log", description: "读取 UE 项目 Saved/Logs 目录下日志的最后 N 行,用于分析崩溃、报错和警告。默认读取 200 行,最大 2000 行。", inputSchema: { type: "object", properties: { lines: { type: "number", description: "要读取的行数,默认 200", default: 200, }, }, required: ["lines"], }, }, { name: "ue58_run_console", description: "在 UE 编辑器批处理模式下执行白名单内的 Console 命令。命令必须严格命中白名单,否则返回拒绝信息。当前只允许 stat fps、stat unit 等稳定命令。", inputSchema: { type: "object", properties: { command: { type: "string", description: "要执行的 Console 命令,必须是白名单中的命令", }, }, required: ["command"], }, }, { name: "ue58_run_python", description: "运行项目 MCP/scripts 目录下的 UE Python 脚本。script 参数是相对 scripts 目录的路径,不允许包含 .. 或绝对路径。", inputSchema: { type: "object", properties: { script: { type: "string", description: "相对 scripts 目录的脚本路径,例如 list_assets.py", }, }, required: ["script"], }, }, ]; server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: TOOLS }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const name = request.params.name; const args = request.params.arguments ?? {}; try { switch (name) { case "ue58_tail_log": { const text = await readProjectLog(Number(args.lines) || 200); return { content: [{ type: "text", text }] }; } case "ue58_run_console": { if (!args.command || typeof args.command !== "string") { throw new Error("缺少 command 参数"); } const output = await runUeCommand(args.command); return { content: [{ type: "text", text: output }] }; } case "ue58_run_python": { if (!args.script || typeof args.script !== "string") { throw new Error("缺少 script 参数"); } const output = await runUePython(args.script); return { content: [{ type: "text", text: output }] }; } default: throw new Error(`未知工具: ${name}`); } } catch (error) { return { isError: true, content: [{ type: "text", text: `工具执行失败: ${error.message}` }], }; } }); const transport = new StdioServerTransport(); await server.connect(transport);这段代码的协议部分比较直接:ListToolsRequestSchema告诉 Claude Code 有哪些工具,CallToolRequestSchema响应具体的工具调用。真正的 UE 交互逻辑被拆分到了ue-bridge.mjs中。
4.4 桥接层 ue-bridge.mjs
桥接层负责执行实际的 UE 操作。设计原则很简单:任何执行 UE 命令或调用 UE Python 脚本的函数,都必须在自己的函数内部完成白名单校验,不能依赖调用方自觉。
import { execFile } from "node:child_process"; import { promisify } from "node:util"; import fs from "node:fs"; import path from "node:path"; const execFileAsync = promisify(execFile); const UE_PROJECT = process.env.UE_PROJECT || ""; const UE_EDITOR = process.env.UE_EDITOR || ""; const UE_SCRIPTS_DIR = process.env.UE_SCRIPTS_DIR || path.join(path.dirname(UE_PROJECT), "MCP", "scripts"); const ALLOWED_CONSOLE_COMMANDS = new Set([ "stat fps", "stat unit", "r.ShaderPrint", ]); function projectLogPath() { const projectDir = path.dirname(UE_PROJECT); const projectName = path.basename(UE_PROJECT, ".uproject"); return path.join(projectDir, "Saved", "Logs", `${projectName}.log`); } export async function readProjectLog(lines = 200) { if (!UE_PROJECT) { throw new Error("未配置 UE_PROJECT 环境变量"); } const logPath = projectLogPath(); if (!fs.existsSync(logPath)) { throw new Error(`日志不存在: ${logPath}`); } const safeLines = Math.max(20, Math.min(lines, 2000)); const content = fs.readFileSync(logPath, "utf8").split("\n"); return content.slice(-safeLines).join("\n"); } export async function runUeCommand(command) { if (!UE_EDITOR) { throw new Error("未配置 UE_EDITOR 环境变量"); } if (!ALLOWED_CONSOLE_COMMANDS.has(command)) { return `拒绝执行: ${command} 不在白名单内,当前白名单: ${[...ALLOWED_CONSOLE_COMMANDS].join(", ")}`; } if (!fs.existsSync(UE_EDITOR)) { throw new Error(`UE 可执行文件不存在: ${UE_EDITOR}`); } const args = [ UE_PROJECT, "-ExecCmds", `${command}; quit`, "-unattended", "-nop4", "-nosplash", ]; const { stdout, stderr } = await execFileAsync(UE_EDITOR, args, { timeout: 60_000, }); return `执行完成。\nstdout:\n${stdout}\nstderr:\n${stderr}`; } export async function runUePython(script) { if (!UE_EDITOR) { throw new Error("未配置 UE_EDITOR 环境变量"); } const scriptsRoot = path.resolve(UE_SCRIPTS_DIR); const fullPath = path.resolve(scriptsRoot, script); if (!fullPath.startsWith(scriptsRoot)) { return `拒绝执行: 脚本必须位于 ${scriptsRoot}`; } if (!fs.existsSync(fullPath)) { return `脚本不存在: ${fullPath}`; } const args = [ UE_PROJECT, `-ExecutePythonScript=${fullPath}`, "-unattended", "-nop4", "-nosplash", "-quit", ]; const { stdout, stderr } = await execFileAsync(UE_EDITOR, args, { timeout: 120_000, }); return `脚本执行完成。\nstdout:\n${stdout}\nstderr:\n${stderr}`; }代码里的ALLOWED_CONSOLE_COMMANDS只是一个最小示例。实际项目中,你应该把stat fps这类诊断命令、资产检查命令、关卡操作命令按需加进去。命令越具体,越容易控制风险。
4.5 最容易写坏的两个细节
第一,不要用child_process.exec拼接用户传入的字符串。exec会启动 shell,字符串里一旦出现&&、;、管道符,命令就可能被打散。推荐使用execFile,它直接把参数数组传给子进程,不经过 shell,也就能减少注入风险。
第二,path.resolve之后一定要用startsWith检查前缀。否则MCP/scripts/../../Scripts/evil.py这种路径可以穿透脚本目录。路径检查必须放在真实文件存在性检查之前。
注意:MCP Server 代码只是本地工具链路的一部分。真正上线前,至少还要检查环境变量来源、命令白名单来源和日志轮转策略。
5. 把 Server 挂到 Claude Code:三种连接方式
5.1 项目级 .mcp.json
Claude Code 支持项目级 MCP 配置。在 UE5.8 项目根目录创建.mcp.json:
{ "mcpServers": { "ue58": { "command": "node", "args": ["D:/ue58-mcp/server/index.mjs"], "env": { "UE_PROJECT": "D:/UnrealProjects/MyGame/MyGame.uproject", "UE_EDITOR": "C:/Program Files/Epic Games/UE_5.8/Engine/Binaries/Win64/UnrealEditor-Cmd.exe", "UE_SCRIPTS_DIR": "D:/UnrealProjects/MyGame/MCP/scripts" } } } }command指定启动 MCP Server 的可执行文件,args传入入口脚本路径,env提供 UE 项目信息。这里有一个细节:如果UE_PROJECT没有配置,Server 即使能启动,调用ue58_tail_log时也会立即报错。所以环境变量是否正确,比工具代码本身更容易成为故障点。
5.2 通过 claude mcp add 注册
除了写入.mcp.json,Claude Code 也提供命令式注册。在项目目录下执行:
cd D:/UnrealProjects/MyGame claude mcp add ue58 --transport stdio -- node D:/ue58-mcp/server/index.mjs需要设置环境变量时,可以先配置好当前 shell 环境,再启动 Claude Code,或者在.mcp.json中显式写入env。命令式注册和项目配置文件都能达到目的,区别在于配置文件的可见性。.mcp.json可以提交到代码仓库,团队成员可以复用同一套配置;命令式注册则更适合个人调试。
5.3 验证列表与连接状态
重新启动 Claude Code 后,执行:
claude mcp list如果看到ue58出现在列表里,说明协议层已经握手成功。再执行:
claude mcp get ue58这个命令会显示更详细的连接配置。如果状态是 failed,需要回去检查 Server 是否能在本机独立运行:
node D:/ue58-mcp/server/index.mjs这一步不会退出,因为 stdio 传输会保持监听。看到进程不结束,至少说明入口脚本没有语法错误。
5.4 在对话里调用 UE 工具
配置完成后,在 Claude Code 的对话中写:
列出 ue58 工具,然后读取当前项目日志最后 80 行。Claude Code 应该会调用ue58_tail_log并返回日志末尾内容。这就是整条链路打通后的最小信号。
注意:不要只验证工具能列出来,还要验证输入参数、输出格式、异常分支和拒绝信息。直接给
ue58_run_console传白名单之外的命令,观察它是否返回拒绝信息,能更早发现安全边界是否生效。
6. 实战:三个 UE 工作流
6.1 场景一:读取日志并定位编辑器崩溃
假设 UE5.8 编辑器在打开某个关卡时崩溃。最直接的操作是先读日志尾部:
读取 MyGame.log 最后 300 行,重点找 Fatal error、Error、Assertion failed 关键字。ue58_tail_log会返回日志文本,Claude 再根据日志内容判断是渲染层崩溃、资产加载问题还是插件冲突。这个场景的收益在于,Claude 能自动完成从“日志读取”到“根因推测”的两步,而不是开发者复制日志后再粘贴进对话。
6.2 场景二:执行白名单编辑器命令
当一个命令被明确加入白名单后,可以这样使用:
执行 stat fps,并说明输出里 FPS 和 Frame 的含义。这里必须再次强调:stat fps这类命令在批处理模式和运行中的编辑器主进程里效果不同。本文示例使用UnrealEditor-Cmd.exe执行批处理命令,适合验证“链路已通”和“命令可执行”。如果你希望命令直接作用于当前打开的编辑器窗口,需要把ue58_run_console改成与运行中的编辑器进程通信,例如通过自定义 UE 插件暴露一个本地命令接口。两种方式没有绝对优劣,区别是批处理脚本简单但启动慢,编辑器插件即时但实现成本高。
6.3 场景三:协调 UE Python 脚本生成资产
如果在MCP/scripts目录下写一个名为list_assets.py的脚本,用于输出当前项目中某个目录下的资产列表,那么 Claude Code 可以这样协作:
运行 MCP/scripts/list_assets.py,把输出整理成资产清单。ue58_run_python只允许执行脚本目录内的文件,所以即使 Claude 尝试传入/tmp/evil.py或../../scripts/evil.py,路径检查也会阻止执行。脚本本身由项目成员维护,Claude 不直接拿到“任意代码执行”的能力,它只是在编排一组已经准备好的自动化任务。